graphql-dynamic v0.0.13
graphql-dynamic
dynamic, schema-less, directive-driven GraphQL
Table of Contents 👇
Usage
npm install graphql-dynamic
import createLoader from 'graphql-dynamic'
const loader = createLoader()
const query = `
{
test @create(value: 1)
}
`
const result = await loader.load(query) // output: { errors: [], infos: [], data: { test: 1 } }
Directives
- graphql 的指令以 @ 符号开头
- 指令按出现顺序执行
- 每个指令都有一个特殊参数
use
,用于动态计算指令参数。
{
# @create 的最终参数来自 use 返回的结果,use 里可以通过 b 访问到其它参数
a @create(b: 1, use: "{ value: b }")
}
fetch|get|post 指令里的 url 参数
{
testUrlString @post(url: "http://example.com/api/name?a=1&b=2")
testUrlObject
@post(
url: { host: "example.com", pathanme: "/api/name", query: { a: 1, b: 2 } }
)
}
fetch|get|post 指令里的 headers 参数
headers 必须是数组[key, value] 格式,而不是 { key: value }。
(graphql 的 key 不允许出现横杠,也不像 json 那样可以用双引号包裹)
{
test
@post(
options: {
headers: [
["Content-Type", "application/json"]
["Accept", "application/json"]
]
}
)
}
@post(url, body, options, bodyType, responseType)
发送 post 请求
- url,可以是 url string,也可以是 url object
- body,post 请求发送的数据
- options, 跟 fetch(url, options) 的 options 结构一致,(除了 headers 要求特殊形式,见“fetch|get|post 指令里的 headers 参数”一节)
- bodyType,发送 post 请求时 body 的编码类型,默认为 json,可以设置为 text 文本格式。
- responseType,获取 post 请求的响应数据的编码类型,默认为 json,可以设置为 text 文本格式。
{
test
@post(
url: "/my/api"
data: { a: 1, b: 2 }
options: {
headers: [
["Content-Type", "application/json"]
["Accept", "application/json"]
]
}
bodyType: "json"
responseType: "json"
)
}
@postAll(url, bodys, options, bodyType, responseType)
发送一系列 post 请求,它内部会以 Promise.all
的形式,等待所有接口响应完成。
- url,可以是 url string,也可以是 url object
- bodys,数组类型,其中的每一个元素都将作为 post 请求发送的 body 数据
- options, 跟 fetch(url, options) 的 options 结构一致,(除了 headers 要求特殊形式,见“fetch|get|post 指令里的 headers 参数”一节)
- bodyType,发送 post 请求时 body 的编码类型,默认为 json,可以设置为 text 文本格式。
- responseType,获取 post 请求的响应数据的编码类型,默认为 json,可以设置为 text 文本格式。
@get(url, query, options, responseType)
发送 get 请求
- url,可以是 url string,也可以是 url object
- query,get 请求发送的数据,会作为
querystring
拼接在 url 的 ? 号后面 - options, 跟 fetch(url, options) 的 options 结构一致,(除了 headers 要求特殊形式,见“fetch|get|post 指令里的 headers 参数”一节)
- responseType,获取 post 请求的响应数据的编码类型,默认为 json,可以设置为 text 文本格式。
{
test
@get(
url: "/my/api"
query: { a: 1, b: 2 }
options: {
headers: [
["Content-Type", "application/json"]
["Accept", "application/json"]
]
}
bodyType: "json"
responseType: "json"
)
}
@getAll(url, querys, options, responseType)
发送一系列 get 请求,内部会用 Promise.all
等待所有接口响应完成
- url,可以是 url string,也可以是 url object
- querys,数组类型,其中的每一个元素都将作为 get 请求发送的数据,会作为
querystring
拼接在 url 的 ? 号后面 - options, 跟 fetch(url, options) 的 options 结构一致,(除了 headers 要求特殊形式,见“fetch|get|post 指令里的 headers 参数”一节)
- responseType,获取 post 请求的响应数据的编码类型,默认为 json,可以设置为 text 文本格式。
@fetch(url, options, bodyType, responseType)
发送 fetch 请求
- url,可以是 url string,也可以是 url object
- options, 跟 fetch(url, options) 的 options 结构一致,(除了 headers 要求特殊形式,见“fetch|get|post 指令里的 headers 参数”一节)
- bodyType,发送 post 请求时 body 的编码类型,默认为 json,可以设置为 text 文本格式。
- responseType,获取 post 请求的响应数据的编码类型,默认为 json,可以设置为 text 文本格式。
{
test
@fetch(
url: "/my/api"
options: {
method: "POST"
body: { a: 1, b: 2 }
headers: [
["Content-Type", "application/json"]
["Accept", "application/json"]
]
}
bodyType: "json"
responseType: "json"
)
}
@create(value)
用 value 参数的值作为当前字段的值
{
number @create(value: 1)
string @create(value: "1")
object @create(value: { a: 1, b: 2 })
array @create(value: [{ a: 1 }, { b: 2 }])
}
返回
{
number: 1,
string: '1',
object: { a: 1, b: 2 }
array: [{ a: 1 }, {b: 2}]
}
@variable(name)
将当前字段的值定义为 graphql 变量
如果 name 参数没有指定,默认为当前字段的名称(fieldName)。
变量的使用,不依赖定义顺序。可以先使用,后定义。子字段可以使用父字段定义的变量,但父字段不能使用子字段的定义的变量。
{
a @create(value: 1) @variable # 将 a 定义为变量
b @create(value: $a) @variable(name: "c") # 使用变量 a,并将 b 定义为变量,变量名为 c
c @create(value: $c) # 使用来自字段 b 定义的变量 c
}
@map(to, ...context)
将当前字段的值映射成另一个,to 参数为一个 js 表达式,在表达式里可以使用 context 里的参数
- to 参数里可以用当前字段的名字访问它的值。
- 如果当前字段的值是对象,则 to 参数里可以用对象里的 key 去访问对应的 value 值
- 如果当前字段的值是数组,则循环这个数组,按上面的规则读取值
- 如果数组的元素也是数组,则继续循环这个数组,按照上面的规则取值
{
a @create(value: 1) @map(to: "a + b", b: 1) # a 最终为 2
objcet @create(value: { a: 1, b: 2 }) @map(to: "{ a: a + 1, b: b + n }", n: 1) # object 最终为 { a: 2, b: 3 }
array @create(value: [{ a: 1 }, { a: 2 }]) @map(to: "{ a: a + 1 }") # array 最终为 [{ a: 2 }, { a: 3 }]
}
@filter(if, ...context)
过滤当前字段的值,if 参数为一个 js 表达式,在表达式里可以使用 context 里的参数
- if 参数里可以用当前字段的名字访问它的值。
- 如果当前字段的值是对象,则 if 参数里可以用对象里的 key 去访问对应的 value 值
- 如果当前字段的值是数组,则循环这个数组,按上面的规则读取值。
- 如果数组的元素也是数组,则继续循环这个数组,按照上面的规则取值
{
a @create(value: 1) @filter(if: "a > 1") # a 不会被输出
b @create(value: 1) @filter(if: "b === 1") # b 输出为 1
objcet @create(value: { a: 1, b: 2 }) @filter(if: "b <= n", n: 2) # object 最终为 { a: 1, b: 2 }
array @create(value: [{ a: 1 }, { a: 2 }]) @filter(if: "a < 2") # array 最终为 [{ a: 1 }]
}
@select(key)
向下遍历查找存在 key 参数指定的字段名的对象,如果存在多个这种对象,收集成数组
当 key 参数不存在时,指令所在的当前字段名为查找的目标 key 值
{
a @create(value: { b: { c: { d: 1 } } }) @select(key: "d")
}
# 输出如下
# {
# a: {
# d: 1
# }
# }
@find(if)
查找当前字段的值,if 参数为一个 js 表达式,在表达式里可以使用 context 里的参数。
- if 参数里可以用当前字段的名字访问它的值。
- 如果当前字段的值是对象,则 if 参数里可以用对象里的 key 去访问对应的 value 值
- 如果当前字段的值是数组,则循环这个数组,按上面的规则读取值。
- 如果数组的元素也是数组,则继续循环这个数组,按照上面的规则取值
和 filter 的差别是,find 指令返回的不是数组,而是第一个匹配的元素。
{
a @create(value: 1) @find(if: "a > 1") # a 不会被输出
b @create(value: 1) @find(if: "b === 1") # b 输出为 1
objcet @create(value: { a: 1, b: 2 }) @find(if: "b <= n", n: 2) # object 最终为 { a: 1, b: 2 }
array @create(value: [{ a: 1 }, { a: 2 }]) @find(if: "a < 2") # array 最终为 { a: 1 }
}
@extend(...object)
用 object 拓展当前的字段值
- 如果当前的字段值不是对象,用 object 替换当前字段的值
- 如果当前字段值为对象,用 object 里的 key 覆盖当前对象的值
- 如果当前对象值为数组,对数组每一项执行 extend 操作
{
a @extend(b: 1, c: 2) # a 输出为 { b: 1, c: 2 }
b @create(value: { b: 0, d: 3 }) @extend(b: 1, c: 2) # b 输出为 { b: 1, c: 2, d: 3 }
c @create(value: [{ b: 0, d: 3 }, { b: -1, d: 4 }]) @extend(b: 1, c: 2) # c 输出为 [{ b: 1, c: 2, d: 3 }, { b: 1, c: 2, d: 4 }]
}
@prepend(value)
从当前字段的数组首位拼接 value 值
{
a @prepend(value: 1)
b @prepend(value: "1")
c @prepend(value: [1, 2])
d @prepend(value: { value: 1 })
}
# 输出
# {
# a: [1],
# b: ['1'],
# c: [1, 2],
# d: [{ value: 1 }]
# }
{
a @create(value: 0) @prepend(value: 1)
b @create(value: "0") @prepend(value: "1")
c @create(value: 0) @prepend(value: [1, 2])
d @create(value: { value: 0 }) @prepend(value: { value: 1 })
e @create(value: [0, 1, 2]) @prepend(value: [3, 4, 5])
}
# 输出
# {
# a: [1, 0],
# b: ['1', '0'],
# c: [1, 2, 0],
# d: [{ value: 1 }, { value: 0 }],
# e: [3, 4, 5, 0, 1, 2]
# }
@append(value)
从当前字段的数组末尾拼接 value 值,用法见 @prepend
Api
graphql-dynamic 基于 graphql-anywhere 实现,部分 api 及概念需参考 graphql-anywhere 文档帮助理解。
createLoader(config)
createLoader 创建查询 graphql 的 loader 对象。
config 参数类型为对象
- variableTimeout 字段表示等待动态的 graphql 变量的超时时间,默认为 3000
- fetchTimeout 字段表示等待 fetch 请求的超时时间,默认为 3000
loader 字段拥有两个方法:
- load(query, variables?, context?, rootValue?)
- query 为 graphql 查询语句(字符串)或者 graphql document,必传
- variables 为传入 graphql 语句的变量对象
- context 为传入 resolver 的 context
- rootValue 为 resolver 开始的根节点的值
- load 方法返回数据格式为 { errors, logs, data } 的 promise 对象
- errors 为数组,包含此次 graphql 查询包含的错误信息
- logs 为数组,包含此次 graphql 查询包含的日志信息(内置的日志为 fetch 的耗时)
- data 为对象,包含我们查询的结果
- use(...middlewares)
- middlewares 为 koa style 的中间件的数组: (ctx, next) -> promise
- ctx 里合并了上述 config 和 context 对象,此外还包含
- fieldName,当前字段名
- rootValue,当前字段的父节点的值
- args 当前字段的参数对象
- context 当前对象
- info 当前字段的附加信息(比如指令,或者 isLeaf 是否枝叶节点)
- result 当前字段的值,默认为 rootValuefieldName,可能被前置中间件(如 @create, @map)进行过更新
- directive(directiveName, directiveHandler) 方法,注册指令,directiveHandler 函数可以获取到指令的 params 参数
- fetch(url, options),同构的 fetch 方法
- error(error) 添加错误信息到响应结果的 errors 数组里
- log(info) 添加信息到响应结果的 logs 数组里
在第一次执行 loader.load 方法之前,可以使用 loader.use 添加自定义中间件。在执行过 loader.load 之后,loader.use 传入的参数会被忽略。
import createLoader from 'graphql-dynamic'
const loader = createLoader({
variableTimeout: 3000,
fetchTimeout: 3000
})
loader.use(async (ctx, next) => {
let start = Date.now()
await next()
console.log('time', Date.now() - start)
})
const result = await loader.load(`{ test @create(value: 1) }`)
// { errors: [], logs: [], data: { test: 1 }}
自定义指令
ctx.directive
方法可以注册一个可用指令,比如 @date 指令实现:
const moment = require('moment')
// @date(format, i18n) 将字段值通过 moment 转换成日期
loader.use((ctx, next) => {
// 注册 @date 指令
ctx.directive('date', params => {
if (!/number|string/.test(typeof ctx.result)) {
return
}
let { format = 'YYYY/MM/DD', i18n = 'zh-cn' } = params
let local = moment(ctx.result)
if (i18n) local.locale(i18n)
ctx.result = local.format(format)
})
return next()
})
配合 expressjs 使用
createGraphql(config) 可用于创建 expressjs 的中间件
config 参数除了包含 createLoader 里的 config 以外,还有 graphql-playground 的设置部分。
- config.endpoint,graphql-playground 里请求的 graphql server 接口地址,默认为
/graphql
import createGraphql from 'graphql-dynamic/express'
const express = require('express')
const app = express()
const playground = {
'general.betaUpdates': false,
'editor.cursorShape': 'line', // possible values: 'line', 'block', 'underline'
'editor.fontSize': 14,
'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`,
'editor.theme': 'light', // possible values: 'dark', 'light'
'editor.reuseHeaders': true, // new tab reuses headers from last tab
'request.credentials': 'omit', // possible values: 'omit', 'include', 'same-origin'
'tracing.hideTracingResponse': true
}
const endpoint = '/graphql'
const router = createGraphql({ endpoint, playground })
app.use(endpoint, router)
// router.loader 可以获取到 loader 对象