Valibot

Valibot 是一个通用的数据验证库,考虑了捆绑包大小、类型安全和开发体验。

@gqloom/valibot 提供了 GQLoom 与 Valibot 的集成,以便将 Valibot Schema 编织成 GraphQL Schema。

安装

npm
yarn
pnpm
bun
npm install @gqloom/core valibot @gqloom/valibot

定义简单标量

在 GQLoom 中,可以使用 valibotSilk 将 Valibot Schema 作为丝线使用:

import * as v from "valibot" import { valibotSilk } from "@gqloom/valibot" const StringScalar = valibotSilk(v.string()) // GraphQLString const BooleanScalar = valibotSilk(v.boolean()) // GraphQLBoolean const FloatScalar = valibotSilk(v.number()) // GraphQLFloat const IntScalar = valibotSilk(v.pipe(v.nullable(v.number()), v.integer())) // GraphQLInt

解析器 | Resolver

为了将 Valibot Schema 作为丝线使用,我们需要为其包裹 valibotSilk,在开发中大量的包裹可能会显得有些繁琐,因此 @gqloom/valibot 提供了重新导出的解析器和操作构造函数来简化这个过程。 从 @gqloom/valibot 引入的 resolverquerymutationfield 将在内部自动包裹 valibotSilk,这样在大部分情况下,我们可以直接使用 Valibot Schema。

import { resolver, query } from "@gqloom/valibot" export const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World!"), })

定义对象

我们可以使用 Valibot 定义对象,并将其作为丝线使用:

import * as v from "valibot" import { collectNames } from "@gqloom/valibot" export const Cat = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }) collectNames({ Cat })

名称和更多元数据

为对象定义名称

GQLoom 中,我们有多种方法来为对象定义名称。

使用__typename 字面量

import * as v from "valibot" export const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), })

在上面的代码中,我们使用 __typename 字面量来为对象定义名称。我们还将 __typename 字面量设置为 nullish,这意味着 __typename 字段是可选的,如果存在,则必须为 "Cat"。

import * as v from "valibot" export const Cat = v.object({ __typename: v.literal("Cat"), name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), })

在上面的代码中,我们仍旧使用 __typename 字面量来为对象定义名称,但这次我们将 __typename 字面量设置为 "Cat",这意味着 __typename 字段是必须的,且必须为 "Cat"。当使用 GraphQL interfaceunion 时,必填的 __typename 将非常有用。

使用collectNames

import * as v from "valibot" import { collectNames } from "@gqloom/valibot" export const Cat = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }) collectNames({ Cat }) // 为 Cat 收集名称,在编织后将呈现在 GraphQL Schema 中

在上面的代码中,我们使用 collectNames 函数来为对象定义名称。collectNames 函数接受一个对象,该对象的键是对象的名称,值是对象本身。

import * as v from "valibot" import { collectNames } from "@gqloom/valibot" export const { Cat } = collectNames({ Cat: v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }), })

在上面的代码中,我们使用 collectNames 函数来为对象定义名称,并将返回的对象解构为 Cat 并导出。

使用asObjectType

import * as v from "valibot" import { asObjectType } from "@gqloom/valibot" export const Cat = v.pipe( v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }), asObjectType({ name: "Cat" }) )

在上面的代码中,我们使用 asObjectType 函数创建一个元数据并将其传入 Valibot 管道中来为对象定义名称。asObjectType 函数接受完整的 GraphQL 对象类型定义,并返回一个元数据。

添加更多元数据

通过 asObjectType 函数,我们可以为对象添加更多元数据,例如 descriptiondeprecationReasonextensions 等。

import * as v from "valibot" import { asObjectType } from "@gqloom/valibot" export const Cat = v.pipe( v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }), asObjectType({ name: "Cat", description: "A cute cat", }) )

在上面的代码中,我们为 Cat 对象添加了一个 description 元数据,该元数据将在 GraphQL Schema 中呈现:

"""A cute cat""" type Cat { name: String! age: Int! loveFish: Boolean }

我们还可以使用 asField 函数为字段添加元数据,例如 descriptiontype 等。

import * as v from "valibot" import { asObjectType, asField } from "@gqloom/valibot" import { GraphQLInt } from "graphql" export const Cat = v.pipe( v.object({ name: v.string(), age: v.pipe( v.number(), asField({ type: GraphQLInt, description: "How old is the cat" }) ), loveFish: v.nullish(v.boolean()), }), asObjectType({ name: "Cat", description: "A cute cat", }) )

在上面的代码中,我们为 age 字段添加了 typedescription 元数据,最终得到如下 GraphQL Schema:

"""A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean }

声明接口

我们还可以使用 asObjectType 函数来声明接口,例如:

import * as v from "valibot" import { asObjectType } from "../src" const Fruit = v.object({ __typename: v.nullish(v.literal("Fruit")), name: v.string(), color: v.string(), prize: v.number(), }) const Orange = v.pipe( v.object({ __typename: v.nullish(v.literal("Orange")), name: v.string(), color: v.string(), prize: v.number(), }), asObjectType({ interfaces: [Fruit] }) )

在上面的代码中,我们使用 asObjectType 函数创建了一个接口 Fruit,并使用 interfaces 选项将 Orange 对象声明为 Fruit 接口的实现。

省略字段

我们还可以使用 asField 函数将 type 设置为 null 来省略字段,例如:

import * as v from "valibot" import { asField } from "@gqloom/valibot" const Dog = v.object({ __typename: v.nullish(v.literal("Dog")), name: v.nullish(v.string()), birthday: v.pipe(v.nullish(v.date()), asField({ type: null })), })

将得到如下 GraphQL Schema:

type Dog { name: String }

定义联合类型

在使用 Valibot 时,我们可以使用 variantunion 定义联合类型。

使用 variant

我们推荐使用 variant 来定义联合类型:

import * as v from "valibot" import { asUnionType } from "@gqloom/valibot" const Cat = v.object({ __typename: v.literal("Cat"), name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }) const Dog = v.object({ __typename: v.literal("Dog"), name: v.string(), age: v.pipe(v.number(), v.integer()), loveBone: v.nullish(v.boolean()), }) const Animal = v.pipe( v.variant("__typename", [Cat, Dog]), asUnionType({ name: "Animal" }) )

在上面的代码中,我们使用 variant 函数创建了一个联合类型。对于 Animal 来说,它通过 __typename 字段来区分具体的类型。

使用 union

我们还可以使用 union 来定义联合类型:

import * as v from "valibot" import { asUnionType, valibotSilk, collectNames } from "@gqloom/valibot" const Cat = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }) const Dog = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveBone: v.nullish(v.boolean()), }) const Animal = v.pipe( v.union([Cat, Dog]), asUnionType({ resolveType: (it) => (it.loveFish ? "Cat" : "Dog"), }) ) collectNames({ Cat, Dog, Animal })

在上面的代码中,我们使用 union 函数创建了一个联合类型。对于 Animal 来说,它通过 resolveType 函数来区分具体的类型。 在这里,如果一个动物它喜欢鱼,那么它就是一只猫,否则就是一只狗。

定义枚举类型

我们可以使用 v.picklistv.enum_ 定义枚举类型。

使用 picklist

通常,我们更推荐使用 v.picklist 来定义枚举类型:

import * as v from "valibot" import { asEnumType, valibotSilk } from "@gqloom/valibot" export const Fruit = v.pipe( v.picklist(["apple", "banana", "orange"]), asEnumType({ name: "Fruit", valuesConfig: { apple: { description: "red" }, banana: { description: "yellow" }, orange: { description: "orange" }, }, }) ) export type IFruit = v.InferOutput<typeof Fruit>

使用 enum_

我们也可以使用 v.enum_ 来定义枚举类型:

export enum Fruit { apple = "apple", banana = "banana", orange = "orange", } export const FruitE = v.pipe( v.enum_(Fruit), asEnumType({ name: "Fruit", valuesConfig: { apple: { description: "red" }, [Fruit.banana]: { description: "yellow" }, [Fruit.orange]: { description: "orange" }, }, }) )

自定义类型映射

为了适应更多的 Valibot 类型,我们可以拓展 GQLoom 为其添加更多的类型映射。

首先我们使用 ValibotWeaver.config 来定义类型映射的配置。这里我们导入来自 graphql-scalarsGraphQLDateTimeGraphQLJSONGraphQLJSONObject 标量,当遇到 dateanyrecord 类型时,我们将其映射到对应的 GraphQL 标量。

import { GraphQLDateTime, GraphQLJSON, GraphQLJSONObject, } from "graphql-scalars" import { ValibotWeaver } from "@gqloom/valibot" export const valibotWeaverConfig = ValibotWeaver.config({ presetGraphQLType: (schema) => { switch (schema.type) { case "date": return GraphQLDateTime case "any": return GraphQLJSON case "record": return GraphQLJSONObject } }, })

在编织 GraphQL Schema 时传入配置到 weave 函数中:

import { weave } from "@gqloom/valibot" export const schema = weave(valibotWeaverConfig, HelloResolver)

默认类型映射

下表列出了 GQLoom 中 Valibot 类型与 GraphQL 类型之间的默认映射关系:

Valibot 类型GraphQL 类型
v.array()GraphQLList
v.bigint()GraphQLInt
v.date()GraphQLString
v.enum_()GraphQLEnumType
v.picklist()GraphQLEnumType
v.literal(false)GraphQLBoolean
v.literal(0)GraphQLFloat
v.literal("")GraphQLString
v.looseObject()GraphQLObjectType
v.object()GraphQLObjectType
v.objectWithRest()GraphQLObjectType
v.strict_object()GraphQLObjectType
v.nonNullable()GraphQLNonNull
v.nonNullish()GraphQLNonNull
v.nonOptional()GraphQLNonNull
v.number()GraphQLFloat
v.pipe(v.number(), v.integer())GraphQLInt
v.string()GraphQLString
v.pipe(v.string(), v.cuid2())GraphQLID
v.pipe(v.string(), v.ulid())GraphQLID
v.pipe(v.string(), v.uuid())GraphQLID
v.union()GraphQLUnionType
v.variant()GraphQLUnionType