Effect
Effect's Schema can describe both types and runtime validation at the same time. @gqloom/effect weaves Effect Schema into GraphQL Schema and reuses existing metadata (title/description/annotations).
Installation
npm i graphql @gqloom/core effect @gqloom/effectpnpm add graphql @gqloom/core effect @gqloom/effectyarn add graphql @gqloom/core effect @gqloom/effectbun add graphql @gqloom/core effect @gqloom/effectdeno add npm:graphql npm:@gqloom/core npm:effect npm:@gqloom/effectDefining simple scalars
In GQLoom, you can directly use Effect Schema as silk:
import { Schema } from "effect"
const standard = Schema.standardSchemaV1
const StringScalar = standard(Schema.String) // GraphQLString
const BooleanScalar = standard(Schema.Boolean) // GraphQLBoolean
const FloatScalar = standard(Schema.Number) // GraphQLFloat
const IntScalar = standard(Schema.Int) // GraphQLInt
const IDScalar = standard(Schema.String.annotations({ identifier: "UUID" })) // GraphQLIDWeave
Use EffectWeaver to let GQLoom understand Effect Schema:
import { weave, resolver, query } from "@gqloom/core"
import { EffectWeaver } from "@gqloom/effect"
import { Schema } from "effect"
const standard = Schema.standardSchemaV1
export const helloResolver = resolver({
hello: query(standard(Schema.String), () => "Hello, World!"),
})
export const schema = weave(EffectWeaver, helloResolver)Defining objects
import { Schema } from "effect"
export const Cat = Schema.Struct({
__typename: Schema.optional(Schema.Literal("Cat")),
name: Schema.String,
age: Schema.Int,
loveFish: Schema.NullOr(Schema.Boolean),
})Names and more metadata
Note
Naming is optional in GQLoom, and GQLoom will automatically name objects based on operation names.
However, explicit naming is the recommended practice in most scenarios.
Defining names for objects
The recommended practice is to use the title metadata in the built-in annotations() of Effect Schema to define a name for the object, for example:
import { Schema } from "effect"
export const Cat = Schema.Struct({
name: Schema.String,
age: Schema.Int,
loveFish: Schema.NullOr(Schema.Boolean),
}).annotations({
title: "Cat",
})We can also use the __typename literal to set a specific value, which is very useful when using GraphQL interface and union, for example:
import { Schema } from "effect"
export const Cat = Schema.Struct({
__typename: Schema.Literal("Cat"), // Required and limited to "Cat"
name: Schema.String,
age: Schema.Int,
loveFish: Schema.NullOr(Schema.Boolean),
})Using collectNames
We can use the collectNames function to define names for objects. The collectNames function accepts an object whose key is the name of the object and whose value is the object itself.
import { collectNames } from "@gqloom/core"
import { Schema } from "effect"
export const Cat = Schema.Struct({
name: Schema.String,
age: Schema.Int,
loveFish: Schema.NullOr(Schema.Boolean),
})
collectNames({ Cat })We can also use the collectNames function to define names for objects and deconstruct the returned objects into Cat and export them.
import { collectNames } from "@gqloom/core"
import { Schema } from "effect"
export const { Cat } = collectNames({
Cat: Schema.Struct({
name: Schema.String,
age: Schema.Int,
loveFish: Schema.NullOr(Schema.Boolean),
}),
})Adding more metadata
import { Schema } from "effect"
import { asField, asObjectType } from "@gqloom/effect"
import { GraphQLInt } from "graphql"
export const Cat = Schema.Struct({
name: Schema.String,
age: Schema.Int.annotations({
[asField]: {
type: GraphQLInt,
description: "How old is the cat",
extensions: {
complexity: 2,
},
},
}),
loveFish: Schema.NullOr(Schema.Boolean),
}).annotations({
[asObjectType]: { name: "Cat", description: "A cute cat" },
})The generated GraphQL Schema:
"""A cute cat"""
type Cat {
name: String!
"""How old is the cat"""
age: Int
loveFish: Boolean
}Declaring interfaces
import { Schema } from "effect"
import { asObjectType } from "@gqloom/effect"
const Node = Schema.Struct({
__typename: Schema.optional(Schema.Literal("Node")),
id: Schema.String,
}).annotations({
title: "Node",
description: "Node interface",
})
const User = Schema.Struct({
__typename: Schema.optional(Schema.Literal("User")),
id: Schema.String,
name: Schema.String,
}).annotations({
title: "User",
[asObjectType]: { interfaces: [Node] },
})Omitting fields
Setting type to null in asField or using field.hidden can hide fields from GraphQL:
import { Schema } from "effect"
import { asField } from "@gqloom/effect"
const Dog = Schema.Struct({
__typename: Schema.optional(Schema.Literal("Dog")),
name: Schema.optional(Schema.String),
birthday: Schema.optional(Schema.Date).annotations({
[asField]: { type: null },
}),
})Defining union types
We recommend naming unions and adding descriptions:
import { Schema } from "effect"
import { asUnionType } from "@gqloom/effect"
const Cat = Schema.Struct({
__typename: Schema.Literal("Cat"),
meow: Schema.String,
})
const Dog = Schema.Struct({
__typename: Schema.Literal("Dog"),
bark: Schema.String,
})
const Animal = Schema.Union(Cat, Dog).annotations({
title: "Animal",
description: "An animal union type",
})EffectWeaver will validate that union members must be object types and automatically handle the impact of null/void/optional members.
Defining enum types
Use Schema.Enums and can attach GraphQL enum metadata:
import { Schema } from "effect"
import { asEnumType } from "@gqloom/effect"
export const Role = Schema.Enums({
Admin: "ADMIN",
User: "USER",
}).annotations({
title: "Role",
[asEnumType]: {
valuesConfig: {
Admin: { description: "Administrator" },
User: { description: "Regular user" },
},
},
})Custom type mapping
Use EffectWeaver.config to provide preset GraphQL types for specific Schema:
import { Schema, SchemaAST } from "effect"
import { EffectWeaver } from "@gqloom/effect"
import { GraphQLDateTime, GraphQLJSON } from "graphql-scalars"
import { weave, resolver, query } from "@gqloom/core"
const standard = Schema.standardSchemaV1
export const effectWeaverConfig = EffectWeaver.config({
presetGraphQLType: (schema) => {
const identifier = SchemaAST.getAnnotation<string>(
SchemaAST.IdentifierAnnotationId
)(schema.ast).pipe((o) => (o._tag === "Some" ? o.value : null))
if (identifier?.includes("Date")) return GraphQLDateTime
if (identifier === "Any" || identifier === "JSON") return GraphQLJSON
},
})
export const helloResolver = resolver({
hello: query(standard(Schema.String), () => "Hello, World!"),
})
export const schema = weave(effectWeaverConfig, helloResolver)Default type mapping
The table below lists the default mapping relationship between GQLoom Effect Schema and GraphQL types (fields with Schema.NullOr / Schema.optional map to nullable types, others are wrapped with GraphQLNonNull by default):
| Effect Type/Feature | GraphQL Type |
|---|---|
Schema.Array / Tuple (first element) | GraphQLList |
Schema.String | GraphQLString |
identifier containing uuid/ulid | GraphQLID |
Schema.Literal("") | GraphQLString |
Schema.Literal(false) | GraphQLBoolean |
Schema.Literal(0) | GraphQLFloat |
Schema.Number | GraphQLFloat |
Schema.Int / Schema.Number + int() | GraphQLInt |
Schema.Boolean | GraphQLBoolean |
Schema.Date / identifier containing Date | GraphQLString |
Schema.Struct / Schema.Record | GraphQLObjectType |
Schema.Enums | GraphQLEnumType |
Schema.Union (object union) | GraphQLUnionType |
Schema.suspend / circular references | resolved to corresponding types normally |