0.0.13 • Published 5 years ago

graphql-dynamic v0.0.13

Weekly downloads
1
License
ISC
Repository
github
Last release
5 years ago

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 对象