diff --git a/.travis.yml b/.travis.yml index ce21122..960bc57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ sudo: false language: node_js node_js: - '8' - - '10' + - '9' install: - npm i npminstall && npminstall script: diff --git a/README.md b/README.md index a45bf94..5f0eed9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # egg-graphql + --- [GraphQL](http://facebook.github.io/graphql/)使用 Schema 来描述数据,并通过制定和实现 GraphQL 规范定义了支持 Schema 查询的 DSQL (Domain Specific Query Language,领域特定查询语言,由 FACEBOOK 提出。 @@ -62,13 +63,13 @@ exports.graphql = { // 是否加载开发者工具 graphiql, 默认开启。路由同 router 字段。使用浏览器打开该可见。 graphiql: true, // graphQL 路由前的拦截器 - onPreGraphQL: function* (ctx) {}, + onPreGraphQL: function*(ctx) {}, // 开发工具 graphiQL 路由前的拦截器,建议用于做权限操作(如只提供开发者使用) - onPreGraphiQL: function* (ctx) {}, + onPreGraphiQL: function*(ctx) {}, }; // 添加中间件拦截请求 -exports.middleware = [ 'graphql' ]; +exports.middleware = ['graphql']; ``` ## 使用方式 @@ -89,23 +90,28 @@ exports.middleware = [ 'graphql' ]; │ │ │ └── schemaDirective.js // 自定义 SchemaDirective │  │  │  │   │   └── user // 一个graphql模型 -│   │   ├── connector.js +│   │   ├── connector.js │   │   ├── resolver.js │   │   └── schema.graphql │   ├── model │   │   └── user.js │   ├── public │   └── router.js - ``` +## ts 的支持 + +## 增加了 schemaDireactives 的支持 + +支持如上约定的目录结构,以及文件名作为指令名的方式. + ## 参考文章 -- [graphql官网](http://facebook.github.io/graphql) +- [graphql 官网](http://facebook.github.io/graphql) -- [如何在egg中使用graphql](https://zhuanlan.zhihu.com/p/30604868) +- [如何在 egg 中使用 graphql](https://zhuanlan.zhihu.com/p/30604868) -- [项目例子:结合sequelize](https://github.com/freebyron/egg-graphql-boilerplate) +- [项目例子:结合 sequelize](https://github.com/freebyron/egg-graphql-boilerplate) ## 协议 diff --git a/agent.js b/agent.js index a4d00a9..31f7978 100644 --- a/agent.js +++ b/agent.js @@ -1,7 +1,5 @@ 'use strict'; module.exports = agent => { - require('./lib/load_schema')(agent); - require('./lib/load_connector')(agent); + require('./lib/loader/graphql-loader')(agent); }; - diff --git a/app.js b/app.js index b6868cc..08303d7 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,5 @@ 'use strict'; module.exports = app => { - require('./lib/load_schema')(app); - require('./lib/load_connector')(app); + require('./lib/loader/graphql-loader')(app); }; - diff --git a/app/extend/context.js b/app/extend/context.js index de5f09e..b1e6a88 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -3,7 +3,6 @@ const SYMBOL_CONNECTOR = Symbol('connector'); module.exports = { - /** * connector instance * @member Context#connector diff --git a/appveyor.yml b/appveyor.yml index 981e82b..d0aa47e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ environment: matrix: - nodejs_version: '8' - - nodejs_version: '10' + - nodejs_version: '9' install: - ps: Install-Product node $env:nodejs_version diff --git a/lib/load_connector.js b/lib/load_connector.js deleted file mode 100644 index 7249c9f..0000000 --- a/lib/load_connector.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const SYMBOL_CONNECTOR_CLASS = Symbol('Application#connectorClass'); - -module.exports = app => { - const basePath = path.join(app.baseDir, 'app/graphql'); - const types = fs.readdirSync(basePath); - - Object.defineProperty(app, 'connectorClass', { - get() { - if (!this[SYMBOL_CONNECTOR_CLASS]) { - const classes = new Map(); - - types.forEach(type => { - const connectorFile = path.join(basePath, type, 'connector.js'); - /* istanbul ignore else */ - if (fs.existsSync(connectorFile)) { - const Connector = require(connectorFile); - classes.set(type, Connector); - } - }); - - this[SYMBOL_CONNECTOR_CLASS] = classes; - } - return this[SYMBOL_CONNECTOR_CLASS]; - }, - }); -}; diff --git a/lib/load_schema.js b/lib/load_schema.js deleted file mode 100644 index 7ca45ca..0000000 --- a/lib/load_schema.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const { - makeExecutableSchema, -} = require('graphql-tools'); -const _ = require('lodash'); - -const SYMBOL_SCHEMA = Symbol('Applicaton#schema'); - -module.exports = app => { - const basePath = path.join(app.baseDir, 'app/graphql'); - const types = fs.readdirSync(basePath); - - const schemas = []; - const resolverMap = {}; - const resolverFactories = []; - const directiveMap = {}; - const schemaDirectivesProps = {}; - - types.forEach(type => { - // Load schema - const schemaFile = path.join(basePath, type, 'schema.graphql'); - /* istanbul ignore else */ - if (fs.existsSync(schemaFile)) { - const schema = fs.readFileSync(schemaFile, { - encoding: 'utf8', - }); - schemas.push(schema); - } - - // Load resolver - const resolverFile = path.join(basePath, type, 'resolver.js'); - if (fs.existsSync(resolverFile)) { - const resolver = require(resolverFile); - if (_.isFunction(resolver)) { - resolverFactories.push(resolver); - } else if (_.isObject(resolver)) { - _.merge(resolverMap, resolver); - } - } - - // Load directive resolver - const directiveFile = path.join(basePath, type, 'directive.js'); - if (fs.existsSync(directiveFile)) { - const directive = require(directiveFile); - _.merge(directiveMap, directive); - } - - // Load schemaDirectives - let schemaDirectivesFile = path.join(basePath, type, 'schemaDirective.js'); - if (fs.existsSync(schemaDirectivesFile)) { - schemaDirectivesFile = require(schemaDirectivesFile); - _.merge(schemaDirectivesProps, schemaDirectivesFile); - } - }); - - Object.defineProperty(app, 'schema', { - get() { - if (!this[SYMBOL_SCHEMA]) { - resolverFactories.forEach(resolverFactory => _.merge(resolverMap, resolverFactory(app))); - - this[SYMBOL_SCHEMA] = makeExecutableSchema({ - typeDefs: schemas, - resolvers: resolverMap, - directiveResolvers: directiveMap, - schemaDirectives: schemaDirectivesProps, - }); - } - return this[SYMBOL_SCHEMA]; - }, - }); -}; diff --git a/lib/loader/graphql-loader.js b/lib/loader/graphql-loader.js new file mode 100644 index 0000000..0ad400f --- /dev/null +++ b/lib/loader/graphql-loader.js @@ -0,0 +1,116 @@ +'use strict'; + +const { join, dirname } = require('path'); +const { merge, isFunction } = require('lodash'); +const is = require('is-type-of'); +const { + makeExecutableSchema, + SchemaDirectiveVisitor, +} = require('graphql-tools'); + +const SYMBOL_SCHEMA = Symbol('Application#schema'); +const SYMBOL_CONNECTOR_CLASS = Symbol('Application#connectorClass'); + +module.exports = app => { + const directiveResolvers = {}; + const schemaDirectives = {}; + const resolvers = {}; + const typeDefs = []; + + class GraphqlLoader { + constructor(app) { + this.app = app; + } + + load() { + const connectorClasses = new Map(); + this.loadGraphql(connectorClasses); + this.loadTypeDefs(); + /** + * create a GraphQL.js GraphQLSchema instance + */ + Object.defineProperties(this.app, { + schema: { + get() { + if (!this[SYMBOL_SCHEMA]) { + this[SYMBOL_SCHEMA] = makeExecutableSchema({ + typeDefs, + resolvers, + directiveResolvers, + schemaDirectives, + }); + } + return this[SYMBOL_SCHEMA]; + }, + }, + connectorClass: { + get() { + if (!this[SYMBOL_CONNECTOR_CLASS]) { + this[SYMBOL_CONNECTOR_CLASS] = connectorClasses; + } + return this[SYMBOL_CONNECTOR_CLASS]; + }, + }, + }); + } + // 加载graphql + loadGraphql(connectorClasses) { + const loader = this.app.loader; + loader.timing.start('Loader Graphql'); + const opt = { + caseStyle: 'lower', + directory: join(this.app.baseDir, 'app/graphql'), + target: {}, + initializer: (obj, opt) => { + const pathName = opt.pathName.split('.').pop(); + // 加载resolver + if (pathName === 'resolver') { + if (isFunction(obj)) { + obj = obj(this.app); + } + merge(resolvers, obj); + } + // load schemaDirective + if (is.class(obj)) { + const proto = Object.getPrototypeOf(obj); + if (proto === SchemaDirectiveVisitor) { + const name = opt.pathName.split('.').pop(); + schemaDirectives[name] = obj; + } + } + if (pathName === 'schemaDirective') { + merge(schemaDirectives, obj); + } + // load directiveResolver + if (pathName === 'directive') { + merge(directiveResolvers, obj); + } + // load connector + if (pathName === 'connector') { + // 获取文件目录名 + const type = dirname(opt.path) + .split(/\/|\\/) + .pop(); + connectorClasses.set(type, obj); + } + }, + }; + new this.app.loader.FileLoader(opt).load(); + loader.timing.end('Loader Graphql'); + } + // 加载typeDefs + loadTypeDefs() { + const opt = { + directory: join(this.app.baseDir, 'app/graphql'), + match: '**/*.graphql', + target: {}, + initializer: obj => { + typeDefs.push(obj.toString('utf8')); + }, + }; + new this.app.loader.FileLoader(opt).load(); + } + } + + new GraphqlLoader(app).load(); +}; diff --git a/package.json b/package.json index e9f2dc7..7c16210 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "agent.js", "config", "app", - "lib" + "lib", + "index.d.ts" ], "ci": { "version": "8, 9" diff --git a/test/app/service/graphql.test.js b/test/app/service/graphql.test.js index 3d97d31..afea8d1 100644 --- a/test/app/service/graphql.test.js +++ b/test/app/service/graphql.test.js @@ -40,19 +40,33 @@ describe('test/plugin.test.js', () => { assert.equal(resp.errors[0].message, 'Unexpected end of JSON input'); }); - it('should return name\'s upperCase with @upper directive', async () => { + it("should return name's upperCase with @upper directive", async () => { const ctx = app.mockContext(); - const resp = await ctx.graphql.query(JSON.stringify({ - query: '{ user(id: 1) { upperName } }', - })); + const resp = await ctx.graphql.query( + JSON.stringify({ + query: '{ user(id: 1) { upperName } }', + }) + ); assert.deepEqual(resp.data, { user: { upperName: 'NAME1' } }); }); - it('should return name\'s lowerCase with schemaDirectives', async () => { + it('should return createAt with @date directive', async () => { const ctx = app.mockContext(); - const resp = await ctx.graphql.query(JSON.stringify({ - query: '{ user(id: 1) { lowerName } }', - })); + const resp = await ctx.service.graphql.query( + JSON.stringify({ + query: '{ user(id: 1) { createAt } }', + }) + ); + assert.deepEqual(resp.data, { user: { createAt: '2018-6-7' } }); + }); + + it("should return name's lowerCase with schemaDirectives", async () => { + const ctx = app.mockContext(); + const resp = await ctx.graphql.query( + JSON.stringify({ + query: '{ user(id: 1) { lowerName } }', + }) + ); assert.deepEqual(resp.data, { user: { lowerName: 'name1' } }); }); }); diff --git a/test/fixtures/apps/graphql-app/app/graphql/directives/date.js b/test/fixtures/apps/graphql-app/app/graphql/directives/date.js new file mode 100644 index 0000000..998d0d4 --- /dev/null +++ b/test/fixtures/apps/graphql-app/app/graphql/directives/date.js @@ -0,0 +1,22 @@ +'use strict'; + +const { SchemaDirectiveVisitor } = require('graphql-tools'); +const { GraphQLString } = require('graphql'); +const moment = require('moment'); + +class FormatDateDirective extends SchemaDirectiveVisitor { + visitFieldDefinition(field) { + const { defaultFormat } = this.args; + field.args.push({ + name: 'format', + type: GraphQLString, + }); + field.resolve = async function(source, args) { + const theDay = moment(source.createAt); + return theDay.format(args.format || defaultFormat); + }; + field.type = GraphQLString; + } +} + +module.exports = FormatDateDirective; diff --git a/test/fixtures/apps/graphql-app/app/graphql/directives/schema.graphql b/test/fixtures/apps/graphql-app/app/graphql/directives/schema.graphql index c3cbc1a..48a8574 100644 --- a/test/fixtures/apps/graphql-app/app/graphql/directives/schema.graphql +++ b/test/fixtures/apps/graphql-app/app/graphql/directives/schema.graphql @@ -1 +1,5 @@ directive @upper on FIELD_DEFINITION + +directive @date(defaultFormat: String = "YYYY-M-D") on FIELD_DEFINITION + +scalar Date diff --git a/test/fixtures/apps/graphql-app/app/graphql/project/schema.graphql b/test/fixtures/apps/graphql-app/app/graphql/project/schema.graphql index 2249d1c..2ae5da5 100644 --- a/test/fixtures/apps/graphql-app/app/graphql/project/schema.graphql +++ b/test/fixtures/apps/graphql-app/app/graphql/project/schema.graphql @@ -1,4 +1,3 @@ - type Project { name: String! } diff --git a/test/fixtures/apps/graphql-app/app/graphql/user/connector.js b/test/fixtures/apps/graphql-app/app/graphql/user/connector.js index 72661d2..b144e0d 100644 --- a/test/fixtures/apps/graphql-app/app/graphql/user/connector.js +++ b/test/fixtures/apps/graphql-app/app/graphql/user/connector.js @@ -10,25 +10,26 @@ class UserConnector { 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: [], - }))); + return Promise.resolve( + ids.map(id => ({ + id, + name: `name${id}`, + upperName: `name${id}`, + password: `password${id}`, + lowerName: `NAME${id}`, + createAt: 1528375304600, + projects: [], + })) + ); } - fetchByIds(ids) { + async fetchByIds(ids) { return this.loader.loadMany(ids); } - fetchById(id) { - return this.loader.load(id); + async fetchById(id) { + return await this.loader.load(id); } - } module.exports = UserConnector; - diff --git a/test/fixtures/apps/graphql-app/app/graphql/user/schema.graphql b/test/fixtures/apps/graphql-app/app/graphql/user/schema.graphql index 0c03c81..d6f569f 100644 --- a/test/fixtures/apps/graphql-app/app/graphql/user/schema.graphql +++ b/test/fixtures/apps/graphql-app/app/graphql/user/schema.graphql @@ -8,6 +8,7 @@ type User { password: String! name: String! upperName: String @upper + createAt: Date @date lowerName: String @lowerCase projects: [Project!] }