npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

graphql-dynamic

v0.0.13

Published

dynamic, schema-less, directive-driven Graphql

Readme

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 当前字段的值,默认为 rootValue[fieldName],可能被前置中间件(如 @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 对象