@ apollo/ core-schema
处理核心图案的字典库
通过 npm 安装 :
npm install @apollo/core-schema
从源创建:
npm install
npm test
快点
分析一个方案
import { Schema, gql } from '@apollo/core-schema'
const schema = Schema.basic(gql`${"example.graphql"}
@link(url: "https://specs.apollo.dev/federation/v1.0")
@link(url: "https://specs.apollo.dev/inaccessible/v0.1")
type User @inaccessible {
id: ID!
}
`);
expect([...schema]).toMatchInlineSnapshot(`
Array [
<>[GraphQL request] 👉@link(url: "https://specs.apollo.dev/federation/v1.0"),
<#User>[GraphQL request] 👉type User @inaccessible {,
]
`);
expect([...schema.scope]).toMatchInlineSnapshot()
expect([...schema.refs]).toMatchInlineSnapshot(`
Array [
<>[example.graphql] 👉@link(url: "https://specs.apollo.dev/federation/v1.0"),
<https://specs.apollo.dev/link/v1.0#@>[example.graphql] 👉@link(url: "https://specs.apollo.dev/federation/v1.0"),
<https://specs.apollo.dev/link/v1.0#@>[example.graphql] 👉@link(url: "https://specs.apollo.dev/inaccessible/v0.1"),
<#User>[example.graphql] 👉type User @inaccessible {,
<https://specs.apollo.dev/inaccessible/v0.1#@>[example.graphql] type User 👉@inaccessible {,
<#ID>[example.graphql] id: 👉ID!,
]
`);
按其全球图表位置查找指令
import {Schema, Defs, GRef, directives} from '@apollo/core-schema'
const schema = Schema.basic(gql `
extend schema
@link(url: "https://spec.example.io/hidden/v1.0", as: "private")
type Product
type Admin @private
type User
`)
const HIDDEN = GRef.rootDirective('https://spec.example.io/hidden/v1.0')
function *hiddenDefs(defs: Defs) {
for (const def of defs) {
for (const directive of directives(def)) {
if (directive.gref === HIDDEN) {
yield def
break
}
}
}
}
expect([...hiddenDefs(schema)].map(def => def.name))
.toEqual(['Admin'])
在核心图中查找名称
得到一个 Schema 从文档中 Schema.from 接下来
通过查询文档名称 schema.scope数字 :
import {Schema, GRef, ref} from '@apollo/core-schema'
const doc = Schema.from(gql `
extend schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://example.com/someSpec/v1.0")
@link(url: "https://spec.example.io/another/v1.0", as: "renamed")
`)
expect(doc.scope.lookup('@link')).toBe(
GRef.rootDirective('https://specs.apollo.dev/link/v1.0')
)
expect(doc.scope.lookup('renamed__Type'))).toBe(
GRef.named('Type', "https://spec.example.io/another/v1.0")
)
构建含有隐含范围的文档
用一套内建文件来解释文档往往有用 链接已经存在于范围.
Scope.from 第二次辩论,所谓的 frame改为:
启用此功能:
const SUBGRAPH_BUILTINS = Schema.from(gql `
extend schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v1.0",
import: "@key @requires @provides @external")
`)
function subgraph(document: DocumentNode) {
return Schema.from(document, SUBGRAPH_BUILTINS)
}
subgraph(gql `
# @key in the next line will be linked to:
#
# https://specs.apollo.dev/federation/v1.0#@key
type User @key(field: "id") {
id: ID!
}
`)
subgraph(gql `
# this will shadow the built-in link to @key:
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: "@key")
# @key in the next line will be linked to:
#
# https://specs.apollo.dev/federation/v2.0#@key
type User @key(field: "id") {
id: ID!
}`)
在文档的链接上显示
function linksFed2(doc: Schema) {
for (const link of doc.scope) {
if (link.gref.graph.satisfies(LinkUrl.from("https://specs.apollo.dev/federation/v2.0"))) {
// child links federation 2.0
return true
}
}
return false
}
expect(
linksFed2(Schema.basicFrom(gql `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0")
`))
).toBe(true)
expect(
linksFed2(Schema.basicFrom(gql `
extend schema @link(url: "https://specs.apollo.dev/federation/v1.9")
`))
).toBe(false)
expect(
linksFed2(Schema.basicFrom(gql ``))
).toBe(false)
文档中的名称标准化
您也许想要在文档中扫描指令, 而不必担心用户是否重新命名了它们 .
页:1 schema.standardize(...urls) 方法可以帮助:
const subgraph = Schema.basic(gql `
@link(url: "https://specs.apollo.dev/federation/v2.0",
# what weird naming choices!
import: """
@key (as @fkey)
@requires (as @frequires)
@provides (as @fprovides)
@tag (as @ftag)
""")
type User @fkey(fields: "id") {
id: ID! @ftag(name: "hi") @tag(name: "my tag")
}
# note: this is our *own* @tag directive, which looks
# just like but means something different than
# federation's @tag:
directive @tag(name: string) on FIELD_DEFINITION
`);
expect(
raw(
// standardize takes LinkUrls and ensures that all references to that schema
// are prefixed with its standard name
subgraph.standardize("https://specs.apollo.dev/federation/v2.0").print()
)
).toMatchInlineSnapshot(`
extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/id/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.0")
type User @federation__key(fields: "id") {
id: ID! @federation__tag(name: "hi") @tag(name: "my tag")
}
directive @tag(name: string) on FIELD_DEFINITION
`);
动机
此库是用来帮助您阅读和操作核心计划 .
背景情况
核心计划 可相互参考元素.
例如,这个计划参考federation 2.0并使用 @key 其指令:
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0")
type User @federation__key(fields: "id") {
id: ID!
}
在这里,我们链接 federation 以它的url 显示 spec。 此链接名称 federation 转到url https://specs.apollo.dev/federation/v2.0,指示核心意识处理器识别像 federation__FieldSet 财务报告和审定财务报表 @federation__key 定义由 https://specs.apollo.dev/federation/v2.0. 互联网档案馆的存檔,存档日期2013-12-22。 。 。 .
@link 推断名称 federation (以及版本) 2.0从url. 也可以明确设置名称:
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", as: fedv2)
type User @fedv2__key(fields: "id") {
id: ID!
}
这些名字会让人讨厌 所以 @link 还提供 import 参数,将非前缀名称链接到远程定义:
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: "@key")
type User @key(fields: "id") {
id: ID!
}
让我们解决冲突 比如说我有这个计划:
type User @key(column: "id") {
id: ID!
}
directive @key(column: string) on OBJECT
现在说我想把这个计划 变成联邦的子图 联邦已经定义 @key 指令,它会和我自己的冲突 @key 指令,这是无关的.
与 @link我可以给联邦的 @key 命令任何我想要的名字, 避免冲突 :
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: "@fedKey: @key")
type User @fedKey(fields: "id") @key(column: "id") {
id: ID!
}
directive @key(column: string) on OBJECT
请注意,这也有利于难民署 @link 指令本身:
extend schema
@coreLink(url: "https://specs.apollo.dev/link/v1.0", as: coreLink)
@coreLink(url: "https://specs.apollo.dev/federation/v2.0",
import: "@fedKey: @key")
汇编
上面的例子不是有效的 GraphQL 计划,因为它们并不包含它们所命名的所有元素的定义。 具体地说,它们没有包含联邦指令的定义,也没有包含联邦指令的定义 @link 自己来 如果您把它们输入到一个需要有效的 GraphQL 方案的工具中, 这个工具将会被打破 .
看来我们应该能解决这个问题. @link 非常像一个 import 语句 - 它的存在似乎意味着某种汇编过程,可以某种方式查阅相关定义并插入到文件中.
这个库提供了这样的机制。 其间,它提供了一个框架,用以与 全球图 定义——从中构建计划,从一个文件复制到另一个文件,等等.
编译器的问题
再来一次
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: "@fedKey: @key")
type User @fedKey(fields: "id") {
id: ID!
}
编译器必须查看此方案,并为文档中引用但未定义的任何元素插入定义。 说我们有一个地图集 里面有一个计划:
extend schema
# @id is @link's sister, specifying this schema's
# position within the global graph
@id(url: "https://specs.apollo.dev/federation/v2.0")
directive @key(fields: FieldSet!) on OBJECT
scalar FieldSet
编辑器需要复制定义 @key 输入文档。 然后它也需要复制定义 FieldSet,从 @key 参考文献 FieldSet. 当它将这些定义插入到文档中时,它需要更改它们的名称,以适应文档的名称空间。 核心计划可以转接 @link 其他核心计划,因此可能涉及增加 @link给其他计划.
这个图书馆揭露了一个编辑模型.
编辑模式
基本办法是:
- 读取一个计划,并通过审查其范围来构建其范围
@link指令。 。 。 范围管理命名空间——它能够查看文档中的任何定义或参考,并将其与全局图表位置(url,基本上是)联系起来。 范围与某一要素在文档中是否有定义完全无关——它的唯一工作是将名称与urls联系起来. - 当复制文档中的节点时,用其全局图位置来注释这些节点及其后人。 我们称之为这个过程 隔离 或者说 非正常化 (因为该数据库携带的元数据)
@link指令被非正常化到整个树上. - 将定义按需要移动而不担心命名空间
- 在发放完成的文件之前,收集所有参考文献,生成适当的
@link信头,以及 重新正常化 它的所有节点, 酌情设置它们的名字.
节点的去正态化和再正态化的过程大多是透明的.
在实践中
您可以构造一个 Schema 从类似 GraphQL 的文档中:
import {Schema, gql} from '@apollo/core-schema'
const schema = Schema.from(gql `
extend schema
@id(url: "https://my/schema")
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation", import: "@key")
@link(url: "https://myorg.internal/future")
type User @key(fields: "id") @future
`)
Schema可执行性,产生文件中的每一项定义:
const defs = [...schema]
Schema总是产生离子树。 分离子树中的定义和参考文献 .gref 属性,在全局图中查找节点:
import {GRef} from '@apollo/core-schema'
expect(defs[defs.length - 1].gref).toBe(
GRef.named('User', 'https://my/schema')
)
(一个"gref"是"g"raph"的"ref".
您可以使用任意机制在文档中插入已解开的节点:
// helper to create a detached @tag directive
function $tag(name: string) {
return {
kind: Kind.DIRECTIVE,
name: "tag",
arguments: [{
name: { kind: Kind.NAME, value: "name" },
value: { kind: Kind.STRING, value: name }
}],
gref: GRef.rootDirective("https://specs.apollo.dev/tag/v0.1")
}
])
// replace @future with @tag(name: "future")
const newSchema = schema.mapDoc(schema =>
visit(schema.document, {
Directive(node) {
if (!hasRef(node)) return
if (node.gref === GRef.rootDirective("https://myorg.internal/future")) {
// replace @future with @tag(name: "future")
return $tag("future")
}
}
}))
终于可以打电话了 compile 重新规范一切并确保适当的 @link 有页眉:
return newSchema.compile()
schema.compile() 使用可选参数, a atlas 委员会将从中设法填补文件中已提及但未列入的任何定义. atlas 它可以是任何可执行的 超越离散的定义。 例如,它可能是另一个 Schema数字 :
const tagSchema = Schema.basic(gql`
@id(url: "https://specs.apollo.dev/tag/v0.1")
directive @tag(name: string) repeatable on OBJECT
`)
return newSchema.compile(tagSchema)
您可以使用 Atlas 将多个计划合并成地图集.
设计原则
AST 重点
这个图书馆以AST为中心与计划合作.
这是很好的,因为AST可以代表许多无法代表的情况 GraphQLSchema例如,没有包含所有定义(本库的原则动机!)的图案不能在其中表示 GraphQL* 类结构。 因此,我们只是不尝试: 这个图书馆从来没有打电话 buildSchema我们也不接触注重执行的班级 GraphQLSchema。 。 。 .
此外,与AST合作也使我们有能力 小数 修改文档而不彻底改变结构。 默认情况下,此处执行的操作试图对文档进行最小的修改,并尽可能保留其结构. alas, 图克解析器的局限性意味着我们目前无法保存注释 .
最后,AST节点被解析器赋予了源位并保留该位置,甚至跨越复杂的变换. 这样做有助于错误报告,也将使生成源图相对容易,尽管我们目前没有这样做.
纯正而不可改变
基本上整个库都是在不可移动的数据结构上作为纯函数执行的,从ASTNodes(我们视其为不可移动)开始. 昂贵的操作被纪念.
懒惰
纯/不可改变/记忆设计的一个后果是,我们一般不计算任何东西,直到我们需要它。 例如, Schema.from 甚至不扫描文档的 @link和构造一个范围,直到 schema.scope 已实际使用。 同样地,节点在被访问之前不会被非正常化.
分类值类型
少数类型,尤其是 GRef, (中文(简体) ), LinkUrl,以及 Version- 已经是 扫描也就是说,它们只能通过记忆性函数来创建,这确保了两个等同的例子永远是同一个例子:
expect(LinkUrl.from('https://specs/example/?extraneous&stuff&ignored'))
.toBe(LinkUrl.from('https://specs/example'))
它们是有效的价值类型,它们可以(和)被使用,例如作为键在 Map编号.