--- url: /docs.md --- # Introduction ## What is GraphQL GraphQL is a query language for APIs, developed and open-sourced by Facebook. It allows clients to specify the required data structure, reducing unnecessary data transfer and improving the performance and maintainability of the API. GraphQL brings the following advantages: * **Type Safety**: Strong type system to ensure the consistency and security of data from the server to the client. * **Flexible Aggregation**: Automatically aggregate multiple queries, reducing the number of client requests and ensuring the simplicity of the server-side API. * **Efficient Querying**: The client can specify the required data structure, reducing unnecessary data transfer and improving the performance and maintainability of the API. * **Easy to Extend**: Extending the API by adding new fields and types without modifying existing code. * **Efficient Collaboration**: Using Schema as documentation, which can reduce communication costs and improve development efficiency in team development. * **Thriving Ecosystem**: Tools and frameworks are emerging constantly. The active community, with diverse applications, is growing fast and has bright prospects. ## What is GQLoom **GQLoom** is a **Code First** GraphQL Schema Loom, used to weave **runtime types** in the **TypeScript/JavaScript** ecosystem into a GraphQL Schema. Runtime validation libraries such as [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), and [Yup](https://github.com/jquense/yup) have been widely used in backend application development. At the same time, when using ORM libraries such as [Prisma](https://www.prisma.io/), [MikroORM](https://mikro-orm.io/), and [Drizzle](https://orm.drizzle.team/), we also pre-define the database table structures or entity models that contain runtime types. The responsibility of GQLoom is to weave these runtime types into a GraphQL Schema. When developing backend applications with GQLoom, you only need to write types using the Schema libraries you are familiar with. Modern Schema libraries will infer TypeScript types for you, and GQLoom will weave GraphQL types for you. In addition, the **resolver factory** of GQLoom can also create CRUD interfaces for [Prisma](./schema/prisma.md#resolver-factory), [MikroORM](./schema/mikro-orm.md#resolver-factory), and [Drizzle](./schema/drizzle.md#resolver-factory), and supports custom input and adding middleware. ::: info The design of GQLoom is inspired by [tRPC](https://trpc.io/) and [TypeGraphQL](https://typegraphql.com/), and some technical implementations draw from [Pothos](https://pothos-graphql.dev/). ::: ### Hello, World ```ts \[zod] twoslash import { query, resolver, weave } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()) .input({ name: z.string().nullish() }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) export const schema = weave(ZodWeaver, helloResolver) ``` ### Highlights you should not miss * 🧑‍💻 **Development Experience**: Fewer boilerplate codes, semantic API design, and extensive ecosystem integration make development enjoyable. * 🔒 **Type Safety**: Automatically infer types from the Schema, enjoy intelligent code completion during development, and detect potential problems during compilation. * 🎯 **Interface Factory**: Ordinary CRUD interfaces are too simple yet too cumbersome. Let the resolver factory create them quickly. * 🔋 **Fully Prepared**: Middleware, context, subscriptions, and federated graphs are ready. * 🔮 **No Magic**: No decorators, no metadata and reflection, no code generation. It can run anywhere with just JavaScript/TypeScript. * 🧩 **Rich Integration**: Use your favorite validation libraries and ORMs to build your next GraphQL application. --- --- url: /docs/getting-started.md --- # Getting Started This guide will help you get started with GQLoom and create a simple GraphQL backend application. ## Prerequisites You only need a JavaScript/TypeScript runtime, such as Node.js, Bun, Deno, or Cloudflare Workers. ## Initialize the project ::: tip Tip If you already have a project, you can skip this step and jump straight to [Installation](#installation). ::: First, create a new folder and initialize the project: ::: code-group ```sh [npm] mkdir gqloom-app # Create a new folder cd ./gqloom-app # Enter the folder npm init -y # Initialize an empty project npm i -D typescript @types/node tsx # Install TypeScript and related dependencies npx tsc --init # Initialize TypeScript configuration ``` ```sh [pnpm] mkdir gqloom-app # Create a new folder cd ./gqloom-app # Enter the folder yarn init -y # Initialize an empty project yarn add -D typescript @types/node tsx # Install TypeScript and related dependencies yarn dlx -q -p typescript tsc --init # Initialize TypeScript configuration ``` ```sh [yarn] mkdir gqloom-app # Create a new folder cd ./gqloom-app # Enter the folder pnpm init # Initialize an empty project pnpm add -D typescript @types/node tsx # Install TypeScript and related dependencies pnpm exec tsc --init # Initialize TypeScript configuration ``` ```sh [bun] mkdir gqloom-app # Create a new folder cd ./gqloom-app # Enter the folder bun init # Initialize the project ``` ```sh [deno] mkdir gqloom-app # Create a new folder cd ./gqloom-app # Enter the folder deno init # Initialize the project ``` ::: ## Installation GQLoom supports multiple runtime types, choose your favorite ORM and validation library! ::: code-group ```sh [npm] npm i graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [pnpm] pnpm add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [yarn] yarn add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [bun] bun add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:valibot npm:@gqloom/valibot ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core zod @gqloom/zod ``` ```sh [pnpm] pnpm add graphql @gqloom/core zod @gqloom/zod ``` ```sh [yarn] yarn add graphql @gqloom/core zod @gqloom/zod ``` ```sh [bun] bun add graphql @gqloom/core zod @gqloom/zod ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:zod npm:@gqloom/zod ``` ::: Please refer to MikroORM's [Quick Start guide](https://mikro-orm.io/docs/quick-start) to install MikroORM and the corresponding database driver. After completing the MikroORM installation, install `@gqloom/mikro-orm`: ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/mikro-orm ``` ::: Please refer to Drizzle's [Getting Started guide](https://orm.drizzle.team/docs/get-started) and [Upgrading to Drizzle v1](https://orm.drizzle.team/docs/upgrade-v1). After completing the Drizzle installation, install `@gqloom/drizzle@rc`: ::: code-group ```sh [npm] npm i graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [pnpm] pnpm add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [yarn] yarn add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [bun] bun add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:drizzle-orm@rc npm:@gqloom/drizzle@rc ``` ::: Please refer to Drizzle's [Getting Started guide](https://orm.drizzle.team/docs/get-started) to install Drizzle and the corresponding database integration. After completing the Drizzle installation, install `@gqloom/drizzle`: ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/drizzle ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/drizzle ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/drizzle ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/drizzle ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/drizzle ``` ::: Please refer to Prisma's [documentation](https://www.prisma.io/docs) to install Prisma and the corresponding database driver. ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/prisma ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/prisma ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/prisma ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/prisma ``` ```sh [deno] deno add npm:graphql npm:prisma npm:@gqloom/core npm:@gqloom/prisma ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core yup @gqloom/yup ``` ```sh [pnpm] pnpm add graphql @gqloom/core yup @gqloom/yup ``` ```sh [yarn] yarn add graphql @gqloom/core yup @gqloom/yup ``` ```sh [bun] bun add graphql @gqloom/core yup @gqloom/yup ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:yup npm:@gqloom/yup ``` ::: Additionally, we need to declare GQLoom metadata for Yup in the project: ```ts [yup.d.ts] import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } ``` ::: code-group ```sh [npm] npm i graphql @gqloom/core effect @gqloom/effect ``` ```sh [pnpm] pnpm add graphql @gqloom/core effect @gqloom/effect ``` ```sh [yarn] yarn add graphql @gqloom/core effect @gqloom/effect ``` ```sh [bun] bun add graphql @gqloom/core effect @gqloom/effect ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:effect npm:@gqloom/effect ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/json ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/json ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/json ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/json ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/json ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core ``` ```sh [pnpm] pnpm add graphql @gqloom/core ``` ```sh [yarn] yarn add graphql @gqloom/core ``` ```sh [bun] bun add graphql @gqloom/core ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core typebox @gqloom/json ``` ```sh [pnpm] pnpm add graphql @gqloom/core typebox @gqloom/json ``` ```sh [yarn] yarn add graphql @gqloom/core typebox @gqloom/json ``` ```sh [bun] bun add graphql @gqloom/core typebox @gqloom/json ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:typebox npm:@gqloom/json ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core arktype @gqloom/json ``` ```sh [pnpm] pnpm add graphql @gqloom/core arktype @gqloom/json ``` ```sh [yarn] yarn add graphql @gqloom/core arktype @gqloom/json ``` ```sh [bun] bun add graphql @gqloom/core arktype @gqloom/json ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:arktype npm:@gqloom/json ``` ::: In addition, we need to choose an [adapter](./advanced/adapters/) to run our GraphQL server.\ Here we choose the [graphql-yoga](https://the-guild.dev/graphql/yoga-server) adapter. ::: code-group ```sh [npm] npm i graphql-yoga ``` ```sh [pnpm] pnpm add graphql-yoga ``` ```sh [yarn] yarn add graphql-yoga ``` ```sh [bun] bun add graphql-yoga ``` ```sh [deno] deno add npm:graphql-yoga ``` ::: ## Hello, World ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import { createYoga } from "graphql-yoga" import * as v from "valibot" const helloResolver = resolver({ hello: query(v.string()) .input({ name: v.nullish(v.string(), "World") }) .resolve(({ name }) => `Hello, ${name}!`), }) const schema = weave(ValibotWeaver, helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" import { createYoga } from "graphql-yoga" import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()) .input({ name: z .string() .nullish() .transform((value) => value ?? "World"), }) .resolve(({ name }) => `Hello, ${name}!`), }) const schema = weave(ZodWeaver, helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` In GQLoom, the easiest way to use `MikroORM` is the [resolver factory](./schema/mikro-orm#resolver-factory),\ with just a few lines you can create a GraphQL app with full CRUD: ::: code-group <<< @/snippets/home/mikro/index.ts{ts twoslash} ```ts twoslash import { mikroSilk } from "@gqloom/mikro-orm" import { defineEntity, type InferEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity), }), }) export interface IPost extends InferEntity {} export const User = mikroSilk(UserEntity) export const Post = mikroSilk(PostEntity) ``` ```GraphQL input BooleanComparisonOperators { """ <@ """ contained: [Boolean!] """ @> """ contains: [Boolean!] """ Equals. Matches values that are equal to a specified value. """ eq: Boolean """ Greater. Matches values that are greater than a specified value. """ gt: Boolean """ Greater or Equal. Matches values that are greater than or equal to a specified value. """ gte: Boolean """ Contains, Contains, Matches any of the values specified in an array. """ in: [Boolean!] """ Lower, Matches values that are less than a specified value. """ lt: Boolean """ Lower or equal, Matches values that are less than or equal to a specified value. """ lte: Boolean """ Not equal. Matches all values that are not equal to a specified value. """ ne: Boolean """ Not contains. Matches none of the values specified in an array. """ nin: [Boolean!] """ && """ overlap: [Boolean!] } input IDComparisonOperators { """ <@ """ contained: [ID!] """ @> """ contains: [ID!] """ Equals. Matches values that are equal to a specified value. """ eq: ID """ Greater. Matches values that are greater than a specified value. """ gt: ID """ Greater or Equal. Matches values that are greater than or equal to a specified value. """ gte: ID """ Contains, Contains, Matches any of the values specified in an array. """ in: [ID!] """ Lower, Matches values that are less than a specified value. """ lt: ID """ Lower or equal, Matches values that are less than or equal to a specified value. """ lte: ID """ Not equal. Matches all values that are not equal to a specified value. """ ne: ID """ Not contains. Matches none of the values specified in an array. """ nin: [ID!] """ && """ overlap: [ID!] } enum MikroOnConflictAction { ignore merge } type Mutation { createPost(data: PostRequiredInput!): Post! createUser(data: UserRequiredInput!): User! deletePost(where: PostFilter): Int! deleteUser(where: UserFilter): Int! insertManyPost(data: [PostRequiredInput]!): [Post!]! insertManyUser(data: [UserRequiredInput]!): [User!]! insertPost(data: PostRequiredInput!): Post! insertUser(data: UserRequiredInput!): User! updatePost(data: PostPartialInput!, where: PostFilter): Int! updateUser(data: UserPartialInput!, where: UserFilter): Int! upsertManyPost( data: [PostPartialInput!]! onConflictAction: MikroOnConflictAction onConflictExcludeFields: [String!] onConflictFields: [String!] onConflictMergeFields: [String!] ): [Post!]! upsertManyUser( data: [UserPartialInput!]! onConflictAction: MikroOnConflictAction onConflictExcludeFields: [String!] onConflictFields: [String!] onConflictMergeFields: [String!] ): [User!]! upsertPost( data: PostPartialInput! onConflictAction: MikroOnConflictAction onConflictExcludeFields: [String!] onConflictFields: [String!] onConflictMergeFields: [String!] ): Post! upsertUser( data: UserPartialInput! onConflictAction: MikroOnConflictAction onConflictExcludeFields: [String!] onConflictFields: [String!] onConflictMergeFields: [String!] ): User! } type Post { author: User createdAt: String! id: ID! published: Boolean! title: String! updatedAt: String! } type PostCursor { endCursor: String hasNextPage: Boolean! hasPrevPage: Boolean! items: [Post!]! length: Int startCursor: String totalCount: Int! } input PostFilter { """ Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. """ AND: [PostFilter!] """ Inverts the effect of a query expression and returns documents that do not match the query expression. """ NOT: PostFilter """ Joins query clauses with a logical OR returns all documents that match the conditions of either clause. """ OR: [PostFilter!] createdAt: StringComparisonOperators id: IDComparisonOperators published: BooleanComparisonOperators title: StringComparisonOperators updatedAt: StringComparisonOperators } input PostOrderBy { createdAt: QueryOrder id: QueryOrder published: QueryOrder title: QueryOrder updatedAt: QueryOrder } input PostPartialInput { author: ID createdAt: String id: ID published: Boolean title: String updatedAt: String } input PostRequiredInput { author: ID! createdAt: String id: ID published: Boolean title: String! updatedAt: String } type Query { countPost(where: PostFilter): Int! countUser(where: UserFilter): Int! findOnePost(offset: Int, orderBy: PostOrderBy, where: PostFilter!): Post findOnePostOrFail( offset: Int orderBy: PostOrderBy where: PostFilter! ): Post! findOneUser(offset: Int, orderBy: UserOrderBy, where: UserFilter!): User findOneUserOrFail( offset: Int orderBy: UserOrderBy where: UserFilter! ): User! findPost( limit: Int offset: Int orderBy: PostOrderBy where: PostFilter ): [Post!]! findPostByCursor( after: String before: String first: Int last: Int orderBy: PostOrderBy where: PostFilter ): PostCursor findUser( limit: Int offset: Int orderBy: UserOrderBy where: UserFilter ): [User!]! findUserByCursor( after: String before: String first: Int last: Int orderBy: UserOrderBy where: UserFilter ): UserCursor } enum QueryOrder { ASC ASC_NULLS_FIRST ASC_NULLS_LAST DESC DESC_NULLS_FIRST DESC_NULLS_LAST } input StringComparisonOperators { """ <@ """ contained: [String!] """ @> """ contains: [String!] """ Equals. Matches values that are equal to a specified value. """ eq: String """ Full text. A driver specific full text search function. """ fulltext: String """ Greater. Matches values that are greater than a specified value. """ gt: String """ Greater or Equal. Matches values that are greater than or equal to a specified value. """ gte: String """ ilike """ ilike: String """ Contains, Contains, Matches any of the values specified in an array. """ in: [String!] """ Like. Uses LIKE operator """ like: String """ Lower, Matches values that are less than a specified value. """ lt: String """ Lower or equal, Matches values that are less than or equal to a specified value. """ lte: String """ Not equal. Matches all values that are not equal to a specified value. """ ne: String """ Not contains. Matches none of the values specified in an array. """ nin: [String!] """ && """ overlap: [String!] """ Regexp. Uses REGEXP operator """ re: String } type User { createdAt: String! email: String! id: ID! name: String! posts(where: PostFilter): [Post!]! role: String! } type UserCursor { endCursor: String hasNextPage: Boolean! hasPrevPage: Boolean! items: [User!]! length: Int startCursor: String totalCount: Int! } input UserFilter { """ Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. """ AND: [UserFilter!] """ Inverts the effect of a query expression and returns documents that do not match the query expression. """ NOT: UserFilter """ Joins query clauses with a logical OR returns all documents that match the conditions of either clause. """ OR: [UserFilter!] createdAt: StringComparisonOperators email: StringComparisonOperators id: IDComparisonOperators name: StringComparisonOperators role: StringComparisonOperators } input UserOrderBy { createdAt: QueryOrder email: QueryOrder id: QueryOrder name: QueryOrder role: QueryOrder } input UserPartialInput { createdAt: String email: String id: ID name: String posts: [ID] role: String } input UserRequiredInput { createdAt: String email: String! id: ID name: String! posts: [ID] role: String } ``` We can also build resolvers from `MikroORM` entities: ```ts twoslash // @paths: {"src/*": ["snippets/home/mikro/*"]} import { field, query, resolver } from "@gqloom/core" import { Post, User } from "src/entities" import * as v from "valibot" export const userResolver = resolver.of(User, { user: query(User.nullable()) // Declare a query that returns a user .input({ id: v.number() }) // This query accepts an `id` as parameter .resolve(async ({ id }) => { const em = await useEm() return await em.findOne(User, id) }), posts: field(Post.list()) // Declare a derived field that returns a list of posts .derivedFrom("posts") // This field depends on the `posts` field .resolve(async (user) => { return await user.posts.loadItems({ dataloader: true }) }), }) // ---cut-after--- import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" const ormPromise = MikroORM.init({ dbName: ":memory:", entities: [User, Post], }) const useEm = createMemoization(async () => (await ormPromise).em.fork()) ``` In GQLoom, the easiest way to use `Drizzle` is the [resolver factory](./schema/drizzle#resolver-factory),\ with just a few lines you can create a GraphQL app with full CRUD: ::: code-group <<< @/snippets/home/drizzle/index.ts{ts twoslash} ```ts twoslash import { drizzleSilk } from "@gqloom/drizzle" import { defineRelations } from "drizzle-orm" import { boolean, integer, pgEnum, pgTable, serial, text, timestamp, varchar, } from "drizzle-orm/pg-core" export const roleEnum = pgEnum("role", ["user", "admin"]) export const User = drizzleSilk( pgTable("users", { id: serial().primaryKey(), createdAt: timestamp().defaultNow(), email: text().unique().notNull(), name: text(), role: roleEnum().default("user"), }) ) export const Post = drizzleSilk( pgTable("posts", { id: serial().primaryKey(), createdAt: timestamp().defaultNow(), updatedAt: timestamp() .defaultNow() .$onUpdateFn(() => new Date()), published: boolean().default(false), title: varchar({ length: 255 }).notNull(), authorId: integer(), }) ) export const relations = defineRelations({ users: User, posts: Post }, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) ``` ```GraphQL type UsersItem { id: Int! createdAt: String email: String! name: String role: String posts: [PostsItem!]! } type PostsItem { id: Int! createdAt: String updatedAt: String published: Boolean title: String! authorId: Int author: UsersItem } type Query { users( offset: Int limit: Int orderBy: [UsersOrderBy!] where: UsersFilters ): [UsersItem!]! usersSingle( offset: Int orderBy: [UsersOrderBy!] where: UsersFilters ): UsersItem posts( offset: Int limit: Int orderBy: [PostsOrderBy!] where: PostsFilters ): [PostsItem!]! postsSingle( offset: Int orderBy: [PostsOrderBy!] where: PostsFilters ): PostsItem } input UsersOrderBy { id: OrderDirection createdAt: OrderDirection email: OrderDirection name: OrderDirection role: OrderDirection } enum OrderDirection { asc desc } input UsersFilters { id: PgSerialFilters createdAt: PgTimestampFilters email: PgTextFilters name: PgTextFilters role: RoleFilters OR: [UsersFiltersOr!] } input PgSerialFilters { eq: Int ne: Int lt: Int lte: Int gt: Int gte: Int inArray: [Int!] notInArray: [Int!] isNull: Boolean isNotNull: Boolean OR: [PgSerialFiltersOr!] } input PgSerialFiltersOr { eq: Int ne: Int lt: Int lte: Int gt: Int gte: Int inArray: [Int!] notInArray: [Int!] isNull: Boolean isNotNull: Boolean } input PgTimestampFilters { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean OR: [PgTimestampFiltersOr!] } input PgTimestampFiltersOr { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean } input PgTextFilters { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean OR: [PgTextFiltersOr!] } input PgTextFiltersOr { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean } input RoleFilters { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean OR: [RoleFiltersOr!] } input RoleFiltersOr { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean } input UsersFiltersOr { id: PgSerialFilters createdAt: PgTimestampFilters email: PgTextFilters name: PgTextFilters role: RoleFilters } input PostsOrderBy { id: OrderDirection createdAt: OrderDirection updatedAt: OrderDirection published: OrderDirection title: OrderDirection authorId: OrderDirection } input PostsFilters { id: PgSerialFilters createdAt: PgTimestampFilters updatedAt: PgTimestampFilters published: PgBooleanFilters title: PgVarcharFilters authorId: PgIntegerFilters OR: [PostsFiltersOr!] } input PgBooleanFilters { eq: Boolean ne: Boolean lt: Boolean lte: Boolean gt: Boolean gte: Boolean inArray: [Boolean!] notInArray: [Boolean!] isNull: Boolean isNotNull: Boolean OR: [PgBooleanFiltersOr!] } input PgBooleanFiltersOr { eq: Boolean ne: Boolean lt: Boolean lte: Boolean gt: Boolean gte: Boolean inArray: [Boolean!] notInArray: [Boolean!] isNull: Boolean isNotNull: Boolean } input PgVarcharFilters { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean OR: [PgVarcharFiltersOr!] } input PgVarcharFiltersOr { eq: String ne: String lt: String lte: String gt: String gte: String like: String notLike: String ilike: String notIlike: String inArray: [String!] notInArray: [String!] isNull: Boolean isNotNull: Boolean } input PgIntegerFilters { eq: Int ne: Int lt: Int lte: Int gt: Int gte: Int inArray: [Int!] notInArray: [Int!] isNull: Boolean isNotNull: Boolean OR: [PgIntegerFiltersOr!] } input PgIntegerFiltersOr { eq: Int ne: Int lt: Int lte: Int gt: Int gte: Int inArray: [Int!] notInArray: [Int!] isNull: Boolean isNotNull: Boolean } input PostsFiltersOr { id: PgSerialFilters createdAt: PgTimestampFilters updatedAt: PgTimestampFilters published: PgBooleanFilters title: PgVarcharFilters authorId: PgIntegerFilters } type Mutation { insertIntoUsers(values: [UsersInsertInput!]!): [UsersItem!]! insertIntoUsersSingle(value: UsersInsertInput!): UsersItem updateUsers(where: UsersFilters, set: UsersUpdateInput!): [UsersItem!]! deleteFromUsers(where: UsersFilters): [UsersItem!]! insertIntoPosts(values: [PostsInsertInput!]!): [PostsItem!]! insertIntoPostsSingle(value: PostsInsertInput!): PostsItem updatePosts(where: PostsFilters, set: PostsUpdateInput!): [PostsItem!]! deleteFromPosts(where: PostsFilters): [PostsItem!]! } input UsersInsertInput { id: Int createdAt: String email: String! name: String role: String } input UsersUpdateInput { id: Int createdAt: String email: String name: String role: String } input PostsInsertInput { id: Int createdAt: String updatedAt: String published: Boolean title: String! authorId: Int } input PostsUpdateInput { id: Int createdAt: String updatedAt: String published: Boolean title: String authorId: Int } ``` We can also build resolvers from `Drizzle` tables: ```ts twoslash // @paths: {"src/*": ["snippets/home/drizzle/*"]} import { field, query, resolver } from "@gqloom/core" import { useSelectedColumns } from "@gqloom/drizzle/context" import { eq, inArray } from "drizzle-orm" import { Post, User } from "src/schema" import * as v from "valibot" export const userResolver = resolver.of(User, { user: query(User.$nullable()) // Declare a query that returns a user .input({ id: v.number() }) // This query accepts an `id` as parameter .resolve(async ({ id }) => { const [user] = await db .select(useSelectedColumns(User)) // Select only the columns that are being queried .from(User) .where(eq(User.id, id)) return user }), posts: field(Post.$list()) // Declare a derived field that returns a list of posts .derivedFrom("id") // This field depends on the `id` field .load(async (users) => { const postList = await db .select(useSelectedColumns(Post)) // Select only the columns that are being queried .from(Post) .where( inArray( Post.authorId, users.map((u) => u.id) ) ) const postMap = Map.groupBy(postList, (p) => p.authorId) return users.map((u) => postMap.get(u.id) ?? []) }), }) // ---cut-after--- import { drizzle } from "drizzle-orm/node-postgres" const db = drizzle(process.env.DATABASE_URL!) ``` In GQLoom, the easiest way to use `Prisma` is the [resolver factory](./schema/prisma#resolver-factory),\ with just a few lines you can create a GraphQL app with full CRUD: ::: code-group ```ts [index.ts] import { createServer } from "node:http" import { weave } from "@gqloom/core" import { PrismaResolverFactory } from "@gqloom/prisma" import { createYoga } from "graphql-yoga" import { PrismaClient } from "./generated/client" import { Post, User } from "./generated/gqloom" const db = new PrismaClient() const userResolver = new PrismaResolverFactory(User, db).resolver() const postResolver = new PrismaResolverFactory(Post, db).resolver() const schema = weave(userResolver, postResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```Prisma [schema.prisma] datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" output = "../src/generated/client" } generator gqloom { provider = "prisma-gqloom" output = "../src/generated/gqloom" } model User { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) email String @unique name String? role Role @default(USER) posts Post[] } model Post { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt published Boolean @default(false) title String @db.VarChar(255) author User? @relation(fields: [authorId], references: [id]) authorId Int? } enum Role { USER ADMIN } ``` ```GraphQL [schema.graphql] type User { id: ID! createdAt: String! email: String! name: String role: Role! posts: [Post!]! } enum Role { USER ADMIN } type Post { id: ID! createdAt: String! updatedAt: String! published: Boolean! title: String! authorId: Int author: User } type Query { countUser(where: UserWhereInput, orderBy: [UserOrderByWithRelationInput!], cursor: UserWhereUniqueInput, skip: Int, take: Int): Int! findFirstUser(where: UserWhereInput, orderBy: [UserOrderByWithRelationInput!], cursor: UserWhereUniqueInput, skip: Int, take: Int, distinct: [UserScalarFieldEnum!]): User findManyUser(where: UserWhereInput, orderBy: [UserOrderByWithRelationInput!], cursor: UserWhereUniqueInput, skip: Int, take: Int, distinct: [UserScalarFieldEnum!]): [User!]! findUniqueUser(where: UserWhereUniqueInput): User countPost(where: PostWhereInput, orderBy: [PostOrderByWithRelationInput!], cursor: PostWhereUniqueInput, skip: Int, take: Int): Int! findFirstPost(where: PostWhereInput, orderBy: [PostOrderByWithRelationInput!], cursor: PostWhereUniqueInput, skip: Int, take: Int, distinct: [PostScalarFieldEnum!]): Post findManyPost(where: PostWhereInput, orderBy: [PostOrderByWithRelationInput!], cursor: PostWhereUniqueInput, skip: Int, take: Int, distinct: [PostScalarFieldEnum!]): [Post!]! findUniquePost(where: PostWhereUniqueInput): Post } input UserWhereInput { AND: [UserWhereInput!] OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IntFilter createdAt: DateTimeFilter email: StringFilter name: StringNullableFilter role: EnumRoleFilter posts: PostListRelationFilter } input IntFilter { equals: Int in: [Int!] notIn: [Int!] lt: Int lte: Int gt: Int gte: Int not: NestedIntFilter } input NestedIntFilter { equals: Int in: [Int!] notIn: [Int!] lt: Int lte: Int gt: Int gte: Int not: NestedIntFilter } input DateTimeFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String not: NestedDateTimeFilter } input NestedDateTimeFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String not: NestedDateTimeFilter } input StringFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String mode: QueryMode not: NestedStringFilter } enum QueryMode { default insensitive } input NestedStringFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String not: NestedStringFilter } input StringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String mode: QueryMode not: NestedStringNullableFilter } input NestedStringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String not: NestedStringNullableFilter } input EnumRoleFilter { equals: Role in: [Role!] notIn: [Role!] not: NestedEnumRoleFilter } input NestedEnumRoleFilter { equals: Role in: [Role!] notIn: [Role!] not: NestedEnumRoleFilter } input PostListRelationFilter { every: PostWhereInput some: PostWhereInput none: PostWhereInput } input PostWhereInput { AND: [PostWhereInput!] OR: [PostWhereInput!] NOT: [PostWhereInput!] id: IntFilter createdAt: DateTimeFilter updatedAt: DateTimeFilter published: BoolFilter title: StringFilter authorId: IntNullableFilter author: UserNullableRelationFilter } input BoolFilter { equals: Boolean not: NestedBoolFilter } input NestedBoolFilter { equals: Boolean not: NestedBoolFilter } input IntNullableFilter { equals: Int in: [Int!] notIn: [Int!] lt: Int lte: Int gt: Int gte: Int not: NestedIntNullableFilter } input NestedIntNullableFilter { equals: Int in: [Int!] notIn: [Int!] lt: Int lte: Int gt: Int gte: Int not: NestedIntNullableFilter } input UserNullableRelationFilter { is: UserWhereInput isNot: UserWhereInput } input UserOrderByWithRelationInput { id: SortOrder createdAt: SortOrder email: SortOrder name: SortOrderInput role: SortOrder posts: PostOrderByRelationAggregateInput } enum SortOrder { asc desc } input SortOrderInput { sort: SortOrder! nulls: NullsOrder } enum NullsOrder { first last } input PostOrderByRelationAggregateInput { _count: SortOrder } input UserWhereUniqueInput { id: Int email: String AND: [UserWhereInput!] OR: [UserWhereInput!] NOT: [UserWhereInput!] createdAt: DateTimeFilter name: StringNullableFilter role: EnumRoleFilter posts: PostListRelationFilter } enum UserScalarFieldEnum { id createdAt email name role } input PostOrderByWithRelationInput { id: SortOrder createdAt: SortOrder updatedAt: SortOrder published: SortOrder title: SortOrder authorId: SortOrderInput author: UserOrderByWithRelationInput } input PostWhereUniqueInput { id: Int AND: [PostWhereInput!] OR: [PostWhereInput!] NOT: [PostWhereInput!] createdAt: DateTimeFilter updatedAt: DateTimeFilter published: BoolFilter title: StringFilter authorId: IntNullableFilter author: UserNullableRelationFilter } enum PostScalarFieldEnum { id createdAt updatedAt published title authorId } type Mutation { createUser(data: UserCreateInput!): User createManyUser(data: [UserCreateManyInput!]!): BatchPayload deleteUser(where: UserWhereUniqueInput!): User deleteManyUser(where: UserWhereInput): BatchPayload updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User! updateManyUser(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! createPost(data: PostCreateInput!): Post createManyPost(data: [PostCreateManyInput!]!): BatchPayload deletePost(where: PostWhereUniqueInput!): Post deleteManyPost(where: PostWhereInput): BatchPayload updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post! updateManyPost(data: PostUpdateManyMutationInput!, where: PostWhereInput): BatchPayload upsertPost(where: PostWhereUniqueInput!, create: PostCreateInput!, update: PostUpdateInput!): Post! } input UserCreateInput { createdAt: String email: String! name: String role: Role posts: PostCreateNestedManyWithoutAuthorInput } input PostCreateNestedManyWithoutAuthorInput { create: [PostCreateWithoutAuthorInput!] connectOrCreate: [PostCreateOrConnectWithoutAuthorInput!] createMany: PostCreateManyAuthorInputEnvelope connect: [PostWhereUniqueInput!] } input PostCreateWithoutAuthorInput { createdAt: String updatedAt: String published: Boolean title: String! } input PostCreateOrConnectWithoutAuthorInput { where: PostWhereUniqueInput! create: PostCreateWithoutAuthorInput! } input PostCreateManyAuthorInputEnvelope { data: [PostCreateManyAuthorInput!]! skipDuplicates: Boolean } input PostCreateManyAuthorInput { id: Int createdAt: String updatedAt: String published: Boolean title: String! } type BatchPayload { count: Int! } input UserCreateManyInput { id: Int createdAt: String email: String! name: String role: Role } input UserUpdateInput { createdAt: DateTimeFieldUpdateOperationsInput email: StringFieldUpdateOperationsInput name: NullableStringFieldUpdateOperationsInput role: EnumRoleFieldUpdateOperationsInput posts: PostUpdateManyWithoutAuthorNestedInput } input DateTimeFieldUpdateOperationsInput { set: String } input StringFieldUpdateOperationsInput { set: String } input NullableStringFieldUpdateOperationsInput { set: String } input EnumRoleFieldUpdateOperationsInput { set: Role } input PostUpdateManyWithoutAuthorNestedInput { create: [PostCreateWithoutAuthorInput!] connectOrCreate: [PostCreateOrConnectWithoutAuthorInput!] upsert: [PostUpsertWithWhereUniqueWithoutAuthorInput!] createMany: PostCreateManyAuthorInputEnvelope set: [PostWhereUniqueInput!] disconnect: [PostWhereUniqueInput!] delete: [PostWhereUniqueInput!] connect: [PostWhereUniqueInput!] update: [PostUpdateWithWhereUniqueWithoutAuthorInput!] updateMany: [PostUpdateManyWithWhereWithoutAuthorInput!] deleteMany: [PostScalarWhereInput!] } input PostUpsertWithWhereUniqueWithoutAuthorInput { where: PostWhereUniqueInput! update: PostUpdateWithoutAuthorInput! create: PostCreateWithoutAuthorInput! } input PostUpdateWithoutAuthorInput { createdAt: DateTimeFieldUpdateOperationsInput updatedAt: DateTimeFieldUpdateOperationsInput published: BoolFieldUpdateOperationsInput title: StringFieldUpdateOperationsInput } input BoolFieldUpdateOperationsInput { set: Boolean } input PostUpdateWithWhereUniqueWithoutAuthorInput { where: PostWhereUniqueInput! data: PostUpdateWithoutAuthorInput! } input PostUpdateManyWithWhereWithoutAuthorInput { where: PostScalarWhereInput! data: PostUpdateManyMutationInput! } input PostScalarWhereInput { AND: [PostScalarWhereInput!] OR: [PostScalarWhereInput!] NOT: [PostScalarWhereInput!] id: IntFilter createdAt: DateTimeFilter updatedAt: DateTimeFilter published: BoolFilter title: StringFilter authorId: IntNullableFilter } input PostUpdateManyMutationInput { createdAt: DateTimeFieldUpdateOperationsInput updatedAt: DateTimeFieldUpdateOperationsInput published: BoolFieldUpdateOperationsInput title: StringFieldUpdateOperationsInput } input UserUpdateManyMutationInput { createdAt: DateTimeFieldUpdateOperationsInput email: StringFieldUpdateOperationsInput name: NullableStringFieldUpdateOperationsInput role: EnumRoleFieldUpdateOperationsInput } input PostCreateInput { createdAt: String updatedAt: String published: Boolean title: String! author: UserCreateNestedOneWithoutPostsInput } input UserCreateNestedOneWithoutPostsInput { create: UserCreateWithoutPostsInput connectOrCreate: UserCreateOrConnectWithoutPostsInput connect: UserWhereUniqueInput } input UserCreateWithoutPostsInput { createdAt: String email: String! name: String role: Role } input UserCreateOrConnectWithoutPostsInput { where: UserWhereUniqueInput! create: UserCreateWithoutPostsInput! } input PostCreateManyInput { id: Int createdAt: String updatedAt: String published: Boolean title: String! authorId: Int } input PostUpdateInput { createdAt: DateTimeFieldUpdateOperationsInput updatedAt: DateTimeFieldUpdateOperationsInput published: BoolFieldUpdateOperationsInput title: StringFieldUpdateOperationsInput author: UserUpdateOneWithoutPostsNestedInput } input UserUpdateOneWithoutPostsNestedInput { create: UserCreateWithoutPostsInput connectOrCreate: UserCreateOrConnectWithoutPostsInput upsert: UserUpsertWithoutPostsInput disconnect: UserWhereInput delete: UserWhereInput connect: UserWhereUniqueInput update: UserUpdateToOneWithWhereWithoutPostsInput } input UserUpsertWithoutPostsInput { update: UserUpdateWithoutPostsInput! create: UserCreateWithoutPostsInput! where: UserWhereInput } input UserUpdateWithoutPostsInput { createdAt: DateTimeFieldUpdateOperationsInput email: StringFieldUpdateOperationsInput name: NullableStringFieldUpdateOperationsInput role: EnumRoleFieldUpdateOperationsInput } input UserUpdateToOneWithWhereWithoutPostsInput { where: UserWhereInput data: UserUpdateWithoutPostsInput! } ``` ::: We can also build resolvers from `Prisma` models: ```ts import { field, query, resolver } from "@gqloom/core" import * as v from "valibot" import { Post, User } from "./generated/gqloom" export const userResolver = resolver.of(User, { user: query(User.nullable()) .input({ id: v.number() }) .resolve(({ id }) => { return db.user.findUnique({ where: { id } }) }), posts: field(Post.list()) .derivedFrom("id") .resolve(async (users) => { return ( (await db.user.findUnique({ where: { id: users.id } }).posts()) ?? [] ) }), }) ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { YupWeaver } from "@gqloom/yup" import { createYoga } from "graphql-yoga" import { string } from "yup" const helloResolver = resolver({ hello: query(string().required()) .input({ name: string().default("World") }) .resolve(({ name }) => `Hello, ${name}!`), }) const schema = weave(YupWeaver, helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { EffectWeaver } from "@gqloom/effect" import { Schema } from "effect" import { createYoga } from "graphql-yoga" const standard = Schema.standardSchemaV1 const helloResolver = resolver({ hello: query(standard(Schema.String)) .input({ name: standard(Schema.NullishOr(Schema.String)), }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) const schema = weave(EffectWeaver, helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { jsonSilk } from "@gqloom/json" import { createYoga } from "graphql-yoga" const helloResolver = resolver({ hello: query(jsonSilk({ type: "string" })) .input({ name: jsonSilk({ type: "string" }) }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) const schema = weave(helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, silk, weave } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" import { createYoga } from "graphql-yoga" const helloResolver = resolver({ hello: query(silk(new GraphQLNonNull(GraphQLString))) .input({ name: silk(GraphQLString) }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) const schema = weave(helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { createServer } from "node:http" import { type GraphQLSilk, query, resolver, weave } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" import { createYoga } from "graphql-yoga" import Type from "typebox" const helloResolver = resolver({ hello: query(typeSilk(Type.String())) .input({ name: typeSilk(Type.String()) }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) const schema = weave(helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) function typeSilk( type: T ): T & GraphQLSilk, Type.Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Type.Static> } ``` ```ts twoslash import { createServer } from "node:http" import { query, resolver, weave } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" import { type } from "arktype" import { createYoga } from "graphql-yoga" const helloResolver = resolver({ hello: query(type("string")) .input(type({ "name?": "string | null" })) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) const schema = weave(JSONWeaver, helloResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ## Next steps * Learn GQLoom's core concepts: [Silk](./silk), [Resolver](./resolver), [Weave](./weave); * Learn common features: [Context](./context), [DataLoader](./dataloader), [Middleware](./middleware) * Add a GraphQL client to your frontend project: [gql.tada](https://gql-tada.0no.co/), [Urql](https://commerce.nearform.com/open-source/urql/), [Apollo Client](https://www.apollographql.com/docs/react), [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/graphql), [Graffle](https://graffle.js.org/) --- --- url: /docs/silk.md --- # Silk The silk is the basic material of the GraphQL Loom, and it reflects both GraphQL types and TypeScript types. At development time, we use the Schema of an existing schema library as the silk, and eventually `GQLoom` will weave the silk into the GraphQL Schema. ## Simple scalar silk We can create a simple scalar silk using the `silk` function: ```ts twoslash import { silk } from "@gqloom/core" import { GraphQLString, GraphQLInt, GraphQLNonNull } from "graphql" const StringSilk = silk(GraphQLString) const IntSilk = silk(GraphQLInt) const NonNullStringSilk = silk(new GraphQLNonNull(GraphQLString)) const NonNullStringSilk1 = silk.nonNull(StringSilk) ``` ## Object silk We can construct GraphQL objects directly using [graphql.js](https://graphql.org/graphql-js/constructing-types/): ```ts twoslash import { silk } from "@gqloom/core" import { GraphQLObjectType, GraphQLNonNull, GraphQLString, GraphQLInt, } from "graphql" interface ICat { name: string age: number } const Cat = silk( new GraphQLObjectType({ name: "Cat", fields: { name: { type: new GraphQLNonNull(GraphQLString) }, age: { type: new GraphQLNonNull(GraphQLInt) }, }, }) ) ``` In the above code: we define an `ICat` interface and a silk named `Cat` using the `silk` function. The `silk` function accepts `ICat` as a generic parameter and also accepts a `GraphQLObjectType` instance to elaborate the structure of `Cat` in GraphQL. `Cat` will be presented in GraphQL as: ```graphql title="GraphQL Schema" type Cat { name: String! age: Int! } ``` You may have noticed that using `graphql.js` to create silk requires the declaration of both the `ICat` interface and the `GraphQLObjectType`, which means that we have created two definitions for `Cat`. Duplicate definitions cost the code simplicity and increased maintenance costs. ## Creating silk using Schema libraries Fortunately, we have Schema libraries like [Valibot](https://valibot.dev/) and [Zod](https://zod.dev/) that create Schemas that carry TypeScript types and still carry types at runtime. `GQLoom` can directly use these Schemas as silk without duplicate definitions. `GQLoom` currently integrates Schemas from the following libraries: * [Valibot](./schema/valibot.md) * [Zod](./schema/zod.md) * [JSON Schema](./schema/json.md) * [Yup](./schema/yup.md) * [Effect Schema](./schema/effect.md) * [Mikro ORM](./schema/mikro-orm.md) * [Drizzle](./schema/drizzle.md) * [Prisma](./schema/prisma.md) Additionally, there are some libraries that can be used as silk through JSON Schema, such as [TypeBox](https://sinclairzx81.github.io/typebox/), [ArkType](https://arktype.io/) etc. ```ts twoslash import * as v from "valibot" const StringSilk = v.string() const BooleanSilk = v.boolean() const Cat = v.object({ __typename: v.literal("Cat"), name: v.string(), age: v.number(), }) ``` We can directly use Valibot Schema as silk, but don't forget to add `ValibotWeaver` from `@gqloom/valibot` when [weaving](./weave.md). ```ts twoslash import { type Loom } from "@gqloom/core" const resolvers: Loom.Resolver[] = [] // ---cut--- import { weave } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" export const schema = weave(ValibotWeaver, ...resolvers) ``` ```ts twoslash import * as z from "zod" const StringSilk = z.string() const BooleanSilk = z.boolean() const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.number(), }) ``` We can directly use Zod Schema as silk, but don't forget to add `ZodWeaver` from `@gqloom/zod` when [weaving](./weave.md). ```ts twoslash import { type Loom } from "@gqloom/core" const resolvers: Loom.Resolver[] = [] // ---cut--- import { weave } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" export const schema = weave(ZodWeaver, ...resolvers) ``` We need to use the `jsonSilk` function from `@gqloom/json` to use JSON Schema as silk: ```ts twoslash import { jsonSilk } from "@gqloom/json" const StringSilk = jsonSilk({ type: "string" }) const BooleanSilk = jsonSilk({ type: "boolean" }) const Cat = jsonSilk({ title: "Cat", type: "object", }) ``` ```ts twoslash import * as yup from "yup" const StringSilk = yup.string() const BooleanSilk = yup.boolean() const Cat = yup.object({ name: yup.string(), age: yup.number(), }).label("Cat") ``` We can directly use Yup Schema as silk, but don't forget to add `YupWeaver` from `@gqloom/yup` when [weaving](./weave.md). ```ts twoslash import { type Loom } from "@gqloom/core" const resolvers: Loom.Resolver[] = [] // ---cut--- import { weave } from "@gqloom/core" import { YupWeaver } from "@gqloom/yup" export const schema = weave(YupWeaver, ...resolvers) ``` To use TypeBox Schema as silk, we need to define a wrapper function for TypeBox Schema using `@gqloom/json`: ```ts twoslash import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" import { type Static, type Type } from "typebox" function typeSilk( type: T ): T & GraphQLSilk, Type.Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Type.Static> } ``` Then we can use the `typeSilk` function to use TypeBox Schema as silk: ```ts twoslash import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" import { type TSchema, type Static } from "typebox" function typeSilk( type: T ): T & GraphQLSilk, Type.Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Type.Static> } // ---cut--- import { Type } from "typebox" const StringSilk = typeSilk(Type.String()) const BooleanSilk = typeSilk(Type.Boolean()) const Cat = typeSilk(Type.Object({ __typename: Type.Optional(Type.Literal("Cat")), name: Type.String(), age: Type.Integer(), })) ``` ```ts twoslash import { type } from "arktype" const StringSilk = type("string") const BooleanSilk = type("boolean") const Cat = type({ "__typename?": "'Cat' | null", name: "string", age: "number", }) ``` We need to use `@gqloom/json` to create a custom `arkTypeWeaver` to use ArkType's Schema as silk: ```ts twoslash import { type Loom } from "@gqloom/core" const resolvers: Loom.Resolver[] = [] // ---cut--- import { type SchemaWeaver, weave } from "@gqloom/core" import { type JSONSchema, JSONWeaver } from "@gqloom/json" import { type Type } from "arktype" const arkTypeWeaver: SchemaWeaver = { vendor: "arktype", getGraphQLType: (type: Type) => { return JSONWeaver.getGraphQLType(type.toJsonSchema() as JSONSchema, { source: type, }) }, } export const schema = weave(arkTypeWeaver, ...resolvers) ``` ```ts twoslash import { Schema } from "effect" const StringSilk = Schema.standardSchemaV1(Schema.String) const BooleanSilk = Schema.standardSchemaV1(Schema.Boolean) const Cat = Schema.standardSchemaV1(Schema.Struct({ name: Schema.String, age: Schema.Int, }).annotations({ title: "Cat" })) ``` We can directly use Effect Schema as silk, but don't forget to add `EffectWeaver` from `@gqloom/effect` when [weaving](./weave.md): ```ts twoslash import { type Loom } from "@gqloom/core" const resolvers: Loom.Resolver[] = [] // ---cut--- import { weave } from "@gqloom/core" import { EffectWeaver } from "@gqloom/effect" export const schema = weave(EffectWeaver, ...resolvers) ``` --- --- url: /docs/resolver.md --- # Resolver A resolver is a place to put GraphQL operations (`query`, `mutation`, `subscription`). Usually we put operations that are close to each other in the same resolver, for example user related operations in a resolver called `userResolver`. ## Distinguishing Operations First, let's take a brief look at the basic operations of GraphQL and when you should use them: * **Query** is an operation that is used to get data, such as getting user information, getting a list of products, and so on. Queries usually do not change the persistent data of the service. * **Mutation** is an operation used to modify data, such as creating a user, updating user information, deleting a user, and so on. Mutation operations usually change the persistent data of a service. * **Subscription** is an operation in which the server actively pushes data to the client. Subscription usually does not change the persistent data of the service. In other words, subscription is a real-time query. ## Defining a resolver We use the `resolver` function to define the resolver: ```ts import { resolver } from "@gqloom/core" const helloResolver = resolver({}) ``` In the code above, we have defined a resolver called `helloResolver`, which has no operations for now. ## Defining operations Let's try to define the operation using the `query` function: ```ts twoslash import { resolver, query, silk } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" const helloResolver = resolver({ hello: query(silk(new GraphQLNonNull(GraphQLString))).resolve( () => "Hello, World" ), }) ``` In the code above, we have defined a `query` operation called `hello` which returns a non-null string. Here, we're using the type definition provided by `graphql.js` directly, which as you can see can be slightly verbose, and we could have chosen to simplify the code by using the schema library: We can define the return type of the `hello` operation using [valibot](./schema/valibot) to define the return type of the `hello` operation: ```ts twoslash import { resolver, query } from "@gqloom/core" import * as v from "valibot" const helloResolver = resolver({ hello: query(v.string()).resolve(() => "Hello, World"), }) ``` In the code above, we use `v.string()` to define the return type of the `hello` operation. We can directly use the `valibot` schema as the `silk`. We can define the return type of the `hello` operation using [zod](./schema/zod) to define the return type of the `hello` operation: ```ts twoslash import { resolver, query } from "@gqloom/core" import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()).resolve(() => "Hello, World"), }) ``` In the code above, we use `z.string()` to define the return type of the `hello` operation. We can directly use the `zod` Schema definition as a `silk`. ## Define the inputs to operations The `query`, `mutation`, and `subscription` operations can all accept input parameters. Let's add an input parameter `name` to the `hello` operation: ```ts twoslash import { resolver, query } from '@gqloom/core' import * as v from "valibot" const helloResolver = resolver({ hello: query(v.string()) .input({ // [!code hl] name: v.nullish(v.string(), "World"), // [!code hl] }) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) ``` In the code above, we use the chained `input` method to define the input parameter: the `input` method accepts an object as a parameter, where the object's keys are the names of the input parameters, and the values are the type definitions of the input parameters. Here, we use `v.nullish(v.string(), “World”)` to define the `name` parameter, which is an optional string with a default value of `“World”`. In the `resolve` function, we can get the value of the input parameter by the first parameter, and TypeScript will derive its type for us, in this case, we directly deconstruct to get the value of the `name` parameter. ```ts twoslash import { resolver, query } from '@gqloom/zod' import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()) .input({ // [!code hl] name: z // [!code hl] .string() // [!code hl] .nullish() // [!code hl] .transform((value) => value ?? "World"), // [!code hl] }) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) ``` In the code above, we use the chained `input` method to define the input parameter: the `input` method accepts an object as a parameter, where the object's keys are the names of the input parameters, and the values are the type definitions of the input parameters. Here, we use `z.string().nullish()` to define the `name` parameter, which is an optional string with a default value of `“World”`. In the `resolve` function, we can get the value of the input parameter by the first parameter, and TypeScript will derive its type for us, in this case, we directly deconstruct to get the value of the `name` parameter. ## Adding more information to operations We can also add more information to the action, such as `description`, `deprecationReason` and `extensions`: ```ts twoslash import { resolver, query } from '@gqloom/core' import * as v from "valibot" const helloResolver = resolver({ hello: query(v.string()) .description("Say hello to someone") // [!code hl] .input({ name: v.nullish(v.string(), "World") }) .resolve(({ name }) => `Hello, ${name}!`), }) ``` ```ts twoslash import { resolver, query } from '@gqloom/zod' import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()) .description("Say hello to someone") // [!code hl] .input({ name: z .string() .nullish() .transform((value) => value ?? "World"), }) .resolve(({ name }) => `Hello, ${name ?? "World"}!`), }) ``` ## Object resolvers In GraphQL, we can define resolvers for fields on an object to add additional properties to the object and create relationships between objects. This allows GraphQL to build very flexible APIs while maintaining simplicity. When using `GQLoom`, we can use the `resolver.of` function to define object resolvers. We start by defining two simple objects `User` and `Book`: ```ts twoslash import * as v from "valibot" const User = v.object({ __typename: v.nullish(v.literal("User")), id: v.number(), name: v.string(), }) interface IUser extends v.InferOutput {} const Book = v.object({ __typename: v.nullish(v.literal("Book")), id: v.number(), title: v.string(), authorID: v.number(), }) interface IBook extends v.InferOutput {} ``` ```ts twoslash import * as z from "zod" const User = z.object({ __typename: z.literal("User").nullish(), id: z.number(), name: z.string(), }) interface IUser extends z.infer {} const Book = z.object({ __typename: z.literal("Book").nullish(), id: z.number(), title: z.string(), authorID: z.number(), }) interface IBook extends z.infer {} ``` In the above code, we have defined two objects `User` and `Book` which represent user and book. In `Book`, we define an `authorID` field, which represents the author ID of the book. In addition, we define two simple Map objects to store some predefined data: ```ts twoslash interface IUser { id: number name: string __typename?: "User" | null | undefined } interface IBook { id: number title: string authorID: number __typename?: "Book" | null | undefined } // ---cut--- const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) ``` Next, we define a `bookResolver`: ```ts twoslash const User = v.object({ __typename: v.nullish(v.literal("User")), id: v.number(), name: v.string(), }) interface IUser extends v.InferOutput {} const Book = v.object({ __typename: v.nullish(v.literal("Book")), id: v.number(), title: v.string(), authorID: v.number(), }) interface IBook extends v.InferOutput {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query } from '@gqloom/core' import * as v from "valibot" const bookResolver = resolver.of(Book, { books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())), }) ``` ```ts twoslash const User = z.object({ __typename: z.literal("User").nullish(), id: z.number(), name: z.string(), }) interface IUser extends z.infer {} const Book = z.object({ __typename: z.literal("Book").nullish(), id: z.number(), title: z.string(), authorID: z.number(), }) interface IBook extends z.infer {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query } from '@gqloom/zod' import * as z from "zod" const bookResolver = resolver.of(Book, { books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())), }) ``` In the above code, we have used the `resolver.of` function to define `bookResolver`, which is an object resolver for resolving `Book` objects. In `bookResolver`, we define a `books` field, which is a query operation to get all the books. Next, we will add an additional field called `author` to the `Book` object to get the author of the book: ```ts twoslash const User = v.object({ __typename: v.nullish(v.literal("User")), id: v.number(), name: v.string(), }) interface IUser extends v.InferOutput {} const Book = v.object({ __typename: v.nullish(v.literal("Book")), id: v.number(), title: v.string(), authorID: v.number(), }) interface IBook extends v.InferOutput {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query, field, mutation } from '@gqloom/core' import * as v from "valibot" const bookResolver = resolver.of(Book, { books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())), author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)), // [!code hl] addBook: mutation(Book) // [!code hl] .input({ title: v.string(), authorID: v.number() }) // [!code hl] .resolve(({ title, authorID }) => { // [!code hl] const id = bookMap.size > 0 ? Math.max(...Array.from(bookMap.keys())) + 1 : 1 // [!code hl] const book: IBook = { id, title, authorID } // [!code hl] bookMap.set(id, book) // [!code hl] return book // [!code hl] }), // [!code hl] }) ``` ```ts twoslash const User = z.object({ __typename: z.literal("User").nullish(), id: z.number(), name: z.string(), }) interface IUser extends z.infer {} const Book = z.object({ __typename: z.literal("Book").nullish(), id: z.number(), title: z.string(), authorID: z.number(), }) interface IBook extends z.infer {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query, field, mutation } from '@gqloom/zod' import * as z from "zod" const bookResolver = resolver.of(Book, { books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())), author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)), // [!code hl] addBook: mutation(Book) // [!code hl] .input({ title: z.string(), authorID: z.number() }) // [!code hl] .resolve(({ title, authorID }) => { // [!code hl] const id = bookMap.size > 0 ? Math.max(...Array.from(bookMap.keys())) + 1 : 1 // [!code hl] const book: IBook = { id, title, authorID } // [!code hl] bookMap.set(id, book) // [!code hl] return book // [!code hl] }), // [!code hl] }) ``` In the above code, we used the `field` function to define the `author` field. The `field` function takes two parameters: * The first argument is the return type of the field; * The second parameter is a parsing function or option, in this case we use a parsing function: we get the `Book` instance from the first parameter of the parsing function, and then we get the corresponding `User` instance from the `userMap` based on the `authorID` field. ### Defining Field Inputs In GraphQL, we can define input parameters for fields in order to pass additional data at query time. In `GQLoom`, we can use the second argument of the `field` function to define the input parameters of a field. ```ts twoslash const User = v.object({ __typename: v.nullish(v.literal("User")), id: v.number(), name: v.string(), }) interface IUser extends v.InferOutput {} const Book = v.object({ __typename: v.nullish(v.literal("Book")), id: v.number(), title: v.string(), authorID: v.number(), }) interface IBook extends v.InferOutput {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query, field, mutation } from '@gqloom/core' import * as v from "valibot" const bookResolver = resolver.of(Book, { books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())), author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)), signature: field(v.string()) // [!code hl] .input({ name: v.string() }) // [!code hl] .resolve((book, { name }) => { // [!code hl] return `The book ${book.title} is in ${name}'s collection.` // [!code hl] }), // [!code hl] addBook: mutation(Book) // [!code hl] .input({ title: v.string(), authorID: v.number() }) // [!code hl] .resolve(({ title, authorID }) => { // [!code hl] const id = bookMap.size > 0 ? Math.max(...Array.from(bookMap.keys())) + 1 : 1 // [!code hl] const book: IBook = { id, title, authorID } // [!code hl] bookMap.set(id, book) // [!code hl] return book // [!code hl] }), // [!code hl] }) ``` ```ts twoslash const User = z.object({ __typename: z.literal("User").nullish(), id: z.number(), name: z.string(), }) interface IUser extends z.infer {} const Book = z.object({ __typename: z.literal("Book").nullish(), id: z.number(), title: z.string(), authorID: z.number(), }) interface IBook extends z.infer {} const userMap: Map = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map = new Map( [ { id: 1, title: "Dream of Red Mansions", authorID: 1 }, { id: 2, title: "Journey to the West", authorID: 2 }, ].map((book) => [book.id, book]) ) // ---cut--- import { resolver, query, field, mutation } from '@gqloom/zod' import * as z from "zod" const bookResolver = resolver.of(Book, { books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())), author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)), signature: field(z.string()) // [!code hl] .input({ name: z.string() }) // [!code hl] .resolve((book, { name }) => { // [!code hl] return `The book ${book.title} is in ${name}'s collection.` // [!code hl] }), // [!code hl] addBook: mutation(Book) // [!code hl] .input({ title: z.string(), authorID: z.number() }) // [!code hl] .resolve(({ title, authorID }) => { // [!code hl] const id = bookMap.size > 0 ? Math.max(...Array.from(bookMap.keys())) + 1 : 1 // [!code hl] const book: IBook = { id, title, authorID } // [!code hl] bookMap.set(id, book) // [!code hl] return book // [!code hl] }), // [!code hl] }) ``` In the above code, we used the `field` function to define the `signature` field. The second argument to the `field` function is an object which contains two fields: * `input`: the input parameter of the field, which is an object containing a `name` field, which is of type `string`; * `resolve`: the field's resolver function, which takes two arguments: the first argument is the source object of the resolver constructed by `resolver.of`, which is an instance of `Book`; the second argument is the field's input parameter, which is an object that contains an input of the `name` field. The `bookResolver` object we just defined can be woven into a GraphQL schema using the [weave](./weave.md) function: ```ts import { weave } from '@gqloom/core' import { ValibotWeaver } from '@gqloom/valibot' export const schema = weave(ValibotWeaver, bookResolver) ``` ```ts import { weave } from '@gqloom/core' import { ZodWeaver } from '@gqloom/zod' export const schema = weave(ZodWeaver, bookResolver) ``` The resulting GraphQL schema is as follows: ```graphql title="GraphQL Schema" type Book { id: ID! title: String! authorID: ID! author: User signature(name: String!): String! } type User { id: ID! name: String! } type Query { books: [Book!]! } type Mutation { addBook(title: String!, authorID: ID!): Book! } ``` ### Define Derived Fields When writing resolvers for database tables or other persistent data, we often need to calculate new fields based on the data in the table, which are derived fields. Derived fields require selecting the data they depend on when retrieving data. We can use `field().derivedFrom()` to declare the dependent data. The derived dependencies will be used by `useResolvingFields()`, and this function is used to accurately obtain the fields required for the current query. ```ts import { field, resolver } from "@gqloom/core" import * as v from "valibot" import { giraffes } from "./table" export const giraffeResolver = resolver.of(giraffes, { age: field(v.number()) .derivedFrom("birthDate") .resolve((giraffe) => { const today = new Date() const age = today.getFullYear() - giraffe.birthDate.getFullYear() return age }), }) ``` ```ts import { field, resolver } from "@gqloom/core" import * as z from "zod" import { giraffes } from "./table" export const giraffeResolver = resolver.of(giraffes, { age: field(z.number()) .derivedFrom("birthDate") .resolve((giraffe) => { const today = new Date() const age = today.getFullYear() - giraffe.birthDate.getFullYear() return age }), }) ``` --- --- url: /docs/weave.md --- # Weave In GQLoom, the `weave` function is used to weave multiple Resolvers or Silks into a single GraphQL Schema. The `weave` function can take [resolver](./resolver.md), [silk](./silk.md), weaver configuration, global [middleware](./middleware.md) ## Weaving resolvers The most common usage is to weave multiple resolvers together, for example: ```ts import { weave } from '@gqloom/core'; export const schema = weave(helloResolver, catResolver); ``` ## Weaving a single silk Sometimes we need to weave a single [silk](./silk.md) woven into a GraphQL Schema, for example: ::: code-group ```ts twoslash [valibot] import { resolver, query, mutation, field } from '@gqloom/core' const helloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), birthDate: v.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) // ---cut--- import { weave } from '@gqloom/core' import { ValibotWeaver } from '@gqloom/valibot' import * as v from "valibot" const Dog = v.object({ __typename: v.nullish(v.literal("Dog")), name: v.string(), age: v.number(), }) export const schema = weave(ValibotWeaver, helloResolver, catResolver, Dog); ``` ```ts twoslash [zod] import { resolver, query, mutation, field } from '@gqloom/core' const helloResolver = resolver({ hello: query(z.string(), () => "Hello, World"), }) const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), birthDate: z.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) // ---cut--- import { weave } from '@gqloom/core' import { ZodWeaver } from '@gqloom/zod' import * as z from "zod" const Dog = z.object({ __typename: z.literal("Dog").nullish(), name: z.string(), age: z.number(), }) export const schema = weave(ZodWeaver, helloResolver, catResolver, Dog); ``` ::: ## Weaver configuration ### Input type naming conversion In GraphQL, objects are recognized as [type](https://graphql.org/graphql-js/object-types/) and [input](https://graphql.org/graphql-js/mutations-and-input-types/). When using `GQLoom`, we usually only use the `object` type, and behind the scenes `GQLoom` will automatically convert the `object` type to the `input` type. The advantage of this is that we can use the `object` type directly to define input parameters without having to define the `input` type manually. However, when we use the same `object` type for both `type` and `input`, it will not be woven into GraphQL Schema due to naming conflict. Let's look at an example: ::: code-group ```ts twoslash [valibot] import { resolver, mutation, weave } from '@gqloom/core' import { ValibotWeaver } from '@gqloom/valibot' import * as v from "valibot" const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), birthDate: v.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) export const schema = weave(ValibotWeaver, catResolver); ``` ```ts twoslash [zod] import { resolver, mutation, weave } from '@gqloom/zod' import { ZodWeaver } from '@gqloom/zod' import * as z from "zod" const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), birthDate: z.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) export const schema = weave(ZodWeaver, catResolver); ``` ::: In the above code, we defined a `Cat` object and used it for `type` and `input`. But when we try to weave `catResolver` into the GraphQL Schema, an error is thrown with a duplicate `Cat` name: ```bash Error: Schema must contain uniquely named types but contains multiple types named "Cat". ``` To solve this problem, we need to specify a different name for the `input` type. We can do this using the `getInputObjectName` option in the `SchemaWeaver.config` configuration: ::: code-group ```ts twoslash [valibot] import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/core' import { ValibotWeaver } from '@gqloom/valibot' import * as v from "valibot" const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), birthDate: v.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) export const schema = weave( catResolver, ValibotWeaver, GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl] ) ``` ```ts twoslash [zod] import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/core' import * as z from "zod" const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), birthDate: z.string(), }) const catResolver = resolver({ createCat: mutation(Cat, { input: { data: Cat, }, resolve: ({ data }) => data, }), }) export const schema = weave( catResolver, GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl] ) ``` ::: Thus, `Cat` objects will be converted to `CatInput` types, thus avoiding naming conflicts. The above `catResolver` will weave the following GraphQL Schema: ```graphql title="GraphQL Schema" type Mutation { createCat(data: CatInput!): Cat! } type Cat { name: String! birthDate: String! } input CatInput { name: String! birthDate: String! } ``` ## Global middleware ```ts import { weave } from '@gqloom/core'; import { logger } from './middlewares'; export const schema = weave(helloResolver, catResolver, logger) ``` See more about middleware usage in [middleware section](./middleware). --- --- url: /docs/context.md --- # Context In the Node.js world, Context allows us to share data and state within the same request. In GraphQL, contexts allow data to be shared between multiple [resolver functions](./resolver) and [middleware](./middleware) for the same request. A common use case is to store the identity of the current visitor in the context to be accessed in the resolver function and middleware. ## Accessing Contexts In `GQLoom`, we access the context through the `useContext()` function. GQLoom's `useContext` function is designed to reference [React](https://react.dev/)'s `useContext` function. You can call the `useContext` function from anywhere within the [resolver](./resolver) to access the context of the current request without explicitly passing the `context` function. Behind the scenes, `useContext` uses Node.js' [AsyncLocalStorage](https://nodejs.js.cn/api/async_context.html#class-asynclocalstorage) to pass the context implicitly. ### Enabling Context ::: info For environments that do not support `AsyncLocalStorage`, such as browsers or Cloudflare Workers, you can use the `context` property in [resolverPayload](./context#access-to-resolver-payload-directly). ::: We enable context by passing `asyncContextProvider` to the `weave` function. `asyncContextProvider` is essentially a global middleware. ```ts import { weave } from "@gqloom/core" import { asyncContextProvider } from "@gqloom/core/context" // [!code hl] const schema = weave(ValibotWeaver, asyncContextProvider, ...resolvers) ``` Next, let's try to access the context in various places. We will use [graphql-yoga](https://the-guild.dev/graphql/yoga-server) as an adapter. ### Accessing contexts in resolve functions ::: code-group ```ts twoslash [valibot] import { query, resolver, weave } from "@gqloom/core" import { useContext } from "@gqloom/core/context" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" const helloResolver = resolver({ hello: query(v.string(), () => { const user = // [!code hl] useContext().request.headers.get("Authorization") // [!code hl] return `Hello, ${user ?? "World"}` }), }) const yoga = createYoga({ schema: weave(ValibotWeaver, helloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash [zod] import { query, resolver, weave } from "@gqloom/core" import { useContext } from "@gqloom/core/context" import { ZodWeaver } from "@gqloom/zod" import * as z from "zod" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" const helloResolver = resolver({ hello: query(z.string(), () => { const user = // [!code hl] useContext().request.headers.get("Authorization") // [!code hl] return `Hello, ${user ?? "World"}` }), }) const yoga = createYoga({ schema: weave(ZodWeaver, helloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ::: In the code above, we use the `useContext` function to get the `Authorization` header of the current request from the context and concatenate it with the `Hello` string. The `useContext` function accepts a generic parameter that specifies the type of the context, in this case we passed in the `YogaInitialContext` type. Let's try to call this query: ```shell curl -X POST http://localhost:4000/graphql -H "content-type: application/json" -H "authorization: Tom" --data-raw '{"query": "query { hello }"}' ``` You should get the following response: ```json {"data":{"hello":"Hello, Tom"}} ``` ### Accessing contexts in middleware ```ts twoslash import { Middleware } from "@gqloom/core" import { useContext } from "@gqloom/core/context" import { type YogaInitialContext } from "graphql-yoga" function useUser() { const user = useContext().request.headers.get("Authorization") return user } const authGuard: Middleware = (next) => { const user = useUser() if (!user) throw new Error("Please login first") return next() } ``` In the code above, we created a custom hook called `useUser` that uses the `useContext` function to get the `Authorization` header of the current request from the context. We then created a middleware called `authGuard` which uses the `useUser` hook to fetch the user and throw an error if the user is not logged in. To learn more about middleware, see [middleware documentation](./middleware). ### Accessing contexts while validating inputs #### Valibot We can customize the validation or transformation in `valibot` and access the context directly within it. ```ts twoslash const UserService = { getUserByAuthorization: async (authorization: string | null) => { return { name: "World" } }, } // ---cut--- import { query, resolver, weave } from "@gqloom/core" import { useContext } from "@gqloom/core/context" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" async function useUser() { const authorization = useContext().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user } const helloResolver = resolver({ hello: query(v.string(), { input: { name: v.pipeAsync( v.nullish(v.string()), v.transformAsync(async (value) => { // [!code hl] if (value != null) return value // [!code hl] const user = await useUser() // [!code hl] return user.name // [!code hl] }) // [!code hl] ), }, resolve: ({ name }) => `Hello, ${name}`, }), }) const yoga = createYoga({ schema: weave(ValibotWeaver, helloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` In the code above, we use `useUser()` in `v.transformAsync` to get the user information in the context and return it as the value of `name`. #### Zod We can customize the validation or transformation in `zod` and access the context directly within it: ```ts twoslash const UserService = { getUserByAuthorization: async (authorization: string | null) => { return { name: "World" } }, } // ---cut--- import { query, resolver, weave } from "@gqloom/core" import { useContext } from "@gqloom/core/context" import { ZodWeaver } from "@gqloom/zod" import * as z from "zod" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" async function useUser() { const authorization = useContext().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user } const helloResolver = resolver({ hello: query(z.string(), { input: { name: z .string() .nullish() .transform(async (value) => { // [!code hl] if (value != null) return value // [!code hl] const user = await useUser() // [!code hl] return user.name // [!code hl] }), // [!code hl] }, resolve: ({ name }) => `Hello, ${name}`, }), }) const yoga = createYoga({ schema: weave(ZodWeaver, helloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` In the code above, we use `useUser()` in `z.transform` to get the user information in the context and return it as the value of `name`. ## Memorization Consider that we access the user through the following custom function: ```ts twoslash const UserService = { getUserByAuthorization: async (authorization: string | null) => { return { name: "World" } }, } // ---cut--- import { useContext } from "@gqloom/core/context" import { type YogaInitialContext } from "graphql-yoga" async function useUser() { const authorization = useContext().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user } ``` We may execute some expensive operations in `useUser()`, such as fetching user information from the database, and we may also call it multiple times in the same request. To avoid the extra overhead of multiple calls, we can use memoization to cache the results and reuse them in subsequent calls. In GQLoom, we use the `createMemoization` function to create a memoized function. A memoization function caches its results in the context after the first call and returns the cached results directly in subsequent calls. That is, the memoized function will only be executed once in the same request, no matter how many times it is called. Let's memoize the `useUser()` function: ```ts twoslash const UserService = { getUserByAuthorization: async (authorization: string | null) => { return { name: "World" } }, } import { type YogaInitialContext } from "graphql-yoga" // ---cut--- import { createMemoization, useContext } from "@gqloom/core/context" const useUser = createMemoization(async () => { const authorization = useContext().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user }) ``` As you can see, we simply wrap the function in the `createMemoization` function. We can then call `useUser()` from anywhere within the resolver without worrying about the overhead of multiple calls. ## Injecting Context `asyncContextProvider` also allows us to inject context. This is typically used in conjunction with [Executors](./advanced/executor). ```ts const giraffeExecutor = giraffeResolver.toExecutor( asyncContextProvider.with(useCurrentUser.provide({ id: 9, roles: ["admin"] })) ) ``` ## Accessing Resolver Payload In addition to the `useContext` function, GQLoom provides the `useResolverPayload` function for accessing all parameters in the resolver: * root: the previous object, not normally used for fields on the root query type; * args: the arguments provided for the field in the GraphQL query; * context: the context object shared across parser functions and middleware, which is the return value of `useContext`; * info: contains information about the current resolver call, such as the path to the GraphQL query, field names, etc; * field: the definition of the field being resolved by the current resolver; ### Access to Resolver Payload directly For environments that do not provide `AsyncLocalStorage`, such as browsers or Cloudflare Workers, we can directly access the resolver payload within resolver functions and middleware. #### Resolver Functions In resolver functions, `payload` is always the last parameter. ::: code-group ```ts [valibot] const helloResolver = resolver({ hello: query(v.string()).resolve((_input, payload) => { const user = // [!code hl] (payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl] return `Hello, ${user ?? "World"}` }), }) ``` ```ts [zod] const helloResolver = resolver({ hello: query(z.string()).resolve((_input, payload) => { const user = // [!code hl] (payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl] return `Hello, ${user ?? "World"}` }), }) ``` ::: #### Middleware ```ts twoslash import { Middleware, ResolverPayload } from "@gqloom/core" import { type YogaInitialContext } from "graphql-yoga" function getUser(payload: ResolverPayload) { const user = (payload.context as YogaInitialContext).request.headers.get( "Authorization" ) return user } const authGuard: Middleware = ({ next, payload }) => { const user = getUser(payload!) if (!user) throw new Error("Please login first") return next() } ``` ## Using Contexts across Adapters In the GraphQL ecosystem, each adapter provides a different context object, and you can learn how to use it in the Adapters chapter: * [Yoga](./advanced/adapters/yoga) * [Apollo](./advanced/adapters/apollo) * [Mercurius](./advanced/adapters/mercurius) --- --- url: /docs/dataloader.md --- # Data Loaders Due to the flexibility of GraphQL, when loading related objects of a certain object, we often need to execute multiple queries. This leads to the notorious N+1 query problem. To solve this, we can use [DataLoader](https://github.com/graphql/dataloader). `DataLoader` can merge multiple requests into a single one, thereby reducing the number of database queries, and also caching query results to avoid redundant queries. ## The N+1 Query Problem Consider a scenario where we need to query all users and their respective posts. Our data table structure is as follows: ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { drizzleSilk } from "@gqloom/drizzle" import { defineRelations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) export const users = drizzleSilk( t.pgTable("users", { id: t.serial().primaryKey(), createdAt: t.timestamp().defaultNow(), email: t.text().unique().notNull(), name: t.text(), role: roleEnum().default("user"), }) ) export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), createdAt: t.timestamp().defaultNow(), updatedAt: t .timestamp() .defaultNow() .$onUpdateFn(() => new Date()), published: t.boolean().default(false), title: t.varchar({ length: 255 }).notNull(), authorId: t.integer().notNull(), }) ) export const relations = defineRelations({ users, posts }, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) ``` A straightforward resolver implementation might look like this: ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { field, query, resolver } from "@gqloom/core" import { eq } from "drizzle-orm" import { db } from "src/db" import { posts, users } from "src/schema" export const userResolver = resolver.of(users, { users: query(users.$list()).resolve(() => db.select().from(users)), posts: field(posts.$list()) .derivedFrom("id") .resolve((user) => db.select().from(posts).where(eq(posts.authorId, user.id)) ), }) ``` When we execute the following query: ```graphql query usersWithPosts { users { id name posts { id title } } } ``` The backend execution flow will be: 1. Execute one query to fetch all user lists (`SELECT * FROM users`). 2. **For each** returned user, execute another query to fetch that user's posts (`SELECT * FROM posts WHERE authorId = ?`). If the first query returns N users, then to fetch their posts, we would collectively execute 1 (fetch users) + N (fetch posts for each user) queries. This is known as the "N+1 Query Problem". When N is large, this puts immense pressure on the database, leading to performance bottlenecks. GQLoom provides powerful tools to elegantly solve this problem. ## `field().load()` Method The simplest way is to use the `field().load()` method. It transforms the resolver function from handling a single parent object to handling a batch of parent objects, allowing for bulk data fetching. The `load` method accepts an asynchronous function as a parameter. The first parameter of this function is an array of parent objects, `parents`, and subsequent parameters are the input arguments `args` for that field. This asynchronous function needs to return an array of the same length as the `parents` array, where each element corresponds to the result for a parent object. ::: info It is crucial that the returned array strictly matches the order and length of the `parents` array. `DataLoader` relies on this order to correctly map results back to each parent object. ::: Let's look at an example. To solve the N+1 problem mentioned above, we can modify the resolver like this: ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { field, resolver } from "@gqloom/core" import { inArray } from "drizzle-orm" import { db } from "src/db" import { posts, users } from "src/schema" export const userResolver = resolver.of(users, { posts: field(posts.$list()) .derivedFrom("id") .load(async (userList) => { // 1. Fetch all posts for the users at once const postList = await db .select() .from(posts) .where( inArray( posts.authorId, userList.map((u) => u.id) ) ) // 2. Group posts by authorId const grouped = Map.groupBy(postList, (p) => p.authorId) // 3. Map the posts back to each user in order return userList.map((u) => grouped.get(u.id) ?? []) }), }) ``` In the code above, the `load` function receives a `userList` array. We extract the `id` of all users and use the `inArray` operation to fetch all related posts from the database in a single query. Then, we group the posts by `authorId` and finally map them back to an array whose order matches `userList`. Thus, regardless of how many users we request, the query to the `posts` table will only be executed once. ## LoomDataLoader `field().load()` is a convenient API provided by GQLoom, which internally creates and manages `DataLoader` instances for us. However, in some scenarios, we might need finer control, or want to share the same data loader instance across different resolvers. In such cases, we can use `LoomDataLoader`. `GQLoom` provides the `LoomDataLoader` abstract class and the `EasyDataLoader` convenience class for creating custom data loaders. ### Custom Data Loaders (LoomDataLoader) We can create a custom data loader by extending `LoomDataLoader` and implementing the `batchLoad` method. ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { field, LoomDataLoader, query, resolver } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { inArray } from "drizzle-orm" import { db } from "src/db" import { posts, users } from "src/schema" import * as v from "valibot" // 1. Create a custom DataLoader export class UserLoader extends LoomDataLoader< number, typeof users.$inferSelect > { protected async batchLoad( keys: number[] ): Promise<(typeof users.$inferSelect | Error)[]> { const userList = await db .select() .from(users) .where(inArray(users.id, keys)) const userMap = new Map(userList.map((u) => [u.id, u])) return keys.map( (key) => userMap.get(key) ?? new Error(`User ${key} not found`) ) } } // 2. Use createMemoization to create a shared loader instance within the request export const useUserLoader = createMemoization(() => new UserLoader()) // 3. Use it in the resolver export const postResolver = resolver.of(posts, { author: field(users) .derivedFrom("authorId") .resolve((post) => { const loader = useUserLoader() return loader.load(post.authorId) }), }) export const userResolver = resolver.of(users, { user: query(users) .input({ id: v.number() }) .resolve(({ id }) => { const loader = useUserLoader() return loader.load(id) }), }) ``` To ensure that each request has an independent data loader instance and to prevent data cache pollution between different requests, we typically combine it with the `createMemoization` function from [Context](./context). This will create a singleton loader within the lifecycle of each request. ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { field, LoomDataLoader, query, resolver } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { inArray } from "drizzle-orm" import { db } from "src/db" import { posts, users } from "src/schema" import * as v from "valibot" // 1. Create a custom DataLoader export class UserLoader extends LoomDataLoader< number, typeof users.$inferSelect > { protected async batchLoad( keys: number[] ): Promise<(typeof users.$inferSelect | Error)[]> { const userList = await db .select() .from(users) .where(inArray(users.id, keys)) const userMap = new Map(userList.map((u) => [u.id, u])) return keys.map( (key) => userMap.get(key) ?? new Error(`User ${key} not found`) ) } } // 2. Use createMemoization to create a shared loader instance within the request export const useUserLoader = createMemoization(() => new UserLoader()) // 3. Use it in the resolver export const postResolver = resolver.of(posts, { author: field(users) .derivedFrom("authorId") .resolve((post) => { const loader = useUserLoader() return loader.load(post.authorId) }), }) export const userResolver = resolver.of(users, { user: query(users) .input({ id: v.number() }) .resolve(({ id }) => { const loader = useUserLoader() return loader.load(id) }), }) ``` In this example, when `useUserLoader()` is called multiple times within the same GraphQL request, it will return the same `UserLoader` instance. Therefore, multiple calls to `loader.load(id)` will be automatically batched, and the `batchLoad` function will only be executed once. ### Convenient Data Loaders (EasyDataLoader) If you are not a fan of object-oriented programming, you can use `EasyDataLoader`. It accepts a `batchLoad` function as a constructor parameter. The `useUserLoader` above can be simplified with `EasyDataLoader`: ```ts twoslash // @paths: {"src/*": ["snippets/dataloader/src/*"]} import { EasyDataLoader, field, resolver } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { inArray } from "drizzle-orm" import { db } from "src/db" import { posts, users } from "src/schema" const useUserLoader = createMemoization(() => { return new EasyDataLoader(async (keys) => { const userList = await db .select() .from(users) .where(inArray(users.id, keys)) const userMap = new Map(userList.map((u) => [u.id, u])) return keys.map( (key) => userMap.get(key) ?? new Error(`User ${key} not found`) ) }) }) // The usage in the resolver remains the same export const postResolver = resolver.of(posts, { author: field(users) .derivedFrom("authorId") .resolve((post) => { const loader = useUserLoader() return loader.load(post.authorId) }), }) ``` --- --- url: /docs/middleware.md --- # Middleware Middleware is a function that intervenes in the processing flow of a parsed function. It provides a way to insert logic into the request and response flow to execute code before a response is sent or before a request is processed. `GQLoom`'s middleware follows the onion middleware pattern of [Koa](https://koajs.com/#application). ## Define Middleware Middleware is a function that will be injected with an `options` object as a parameter when called. The `options` object contains the following fields: * `outputSilk`: output silk, which includes the output type of the field currently being parsed; * `parent`: the parent node of the current field, equivalent to `useResolverPayload().root`; * `parseInput`: a function used to obtain or modify the input of the current field; * `type`: the type of the current field, whose value can be `query`, `mutation`, `subscription`, or `field`; * `next`: a function used to call the next middleware; The `options` object can also be directly used as the `next` function. Additionally, we can use `useContext()` and `useResolverPayload()` to get the context and more information of the current resolver function. A minimal middleware function is as follows: ```ts twoslash import { Middleware } from '@gqloom/core'; const middleware: Middleware = async (next) => { return await next(); } ``` Next, we'll introduce some common types of middleware. ### JSON Schema Input Validation ::: tip For input validation libraries that follow the [Standard Schema](https://standardschema.dev/), GQLoom will internally call their provided validation functions to validate the input, eliminating the need for additional input validation middleware. ::: When using JSON Schema, we can use [Ajv](https://ajv.js.org/) for runtime validation of the input: ```ts twoslash import { isSilk, type Middleware } from "@gqloom/core" import type { JSONSchema, JSONSilk } from "@gqloom/json" import Ajv, { type ValidateFunction } from "ajv" import { GraphQLError } from "graphql" // Initialize Ajv instance for JSON schema validation. const ajv = new Ajv() // Cache compiled validation functions to avoid recompilation for the same schema. const validators = new WeakMap() function validateAndThrow(schema: JSONSchema & object, value: any) { // Retrieve compiled validator from cache or compile it if not found. const validate = validators.get(schema) ?? ajv.compile(schema) // Cache the compiled validator if it wasn't already there. if (!validators.has(schema)) validators.set(schema, validate) const valid = validate(value) if (!valid) { // If validation fails, throw a GraphQLError with details. throw new GraphQLError(validate.errors?.[0]?.message ?? "Invalid input", { extensions: { issues: validate.errors }, }) } } export const inputValidator: Middleware = async ({ next, parseInput }) => { const schema: | JSONSilk | Record> | undefined = parseInput.schema // If input is already parsed or no schema is provided, skip validation. if (parseInput.result || !schema) return next() // Handle single JSONSilk schema validation. if (isSilk(schema)) { validateAndThrow(schema, parseInput.value) } else { // Handle validation for a map of schemas (e.g., for multiple arguments). for (const [key, argSchema] of Object.entries(schema)) { validateAndThrow(argSchema, parseInput.value[key]) } } return next() } ``` ### Validate output In `GQLoom`, validation of parser output is not performed by default. However, we can validate the output of parser functions through middleware. ```ts twoslash import { silk, type Middleware } from "@gqloom/core" import { GraphQLError } from "graphql" export const outputValidator: Middleware = async (opts) => { const output = await opts.next() const result = await silk.parse(opts.outputSilk, output) if (result.issues) { throw new GraphQLError(result.issues[0].message, { extensions: { issues: result.issues }, }) } return result.value } ``` Let's try to use this middleware: #### Valibot ```ts twoslash // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export const outputValidator: Middleware = (next) => next() export const valibotExceptionFilter: Middleware = (next) => next() // @filename: main.ts // ---cut--- import { ValibotWeaver, weave, resolver, query } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" import { outputValidator, valibotExceptionFilter } from "./middlewares" const helloResolver = resolver({ hello: query(v.pipe(v.string(), v.minLength(10))) .input({ name: v.string() }) .use(outputValidator) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) export const schema = weave(ValibotWeaver, helloResolver, valibotExceptionFilter) // [!code hl] const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { // eslint-disable-next-line no-console console.info("Server is running on http://localhost:4000/graphql") }) ``` In the code above, we added the `v.minLength(10)` requirement to the output of the `hello` query and added the `outputValidator` middleware to the parser function. We also added a global middleware `ValibotExceptionFilter` to `weave`. #### Zod ```ts twoslash // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export const outputValidator: Middleware = (next) => next() export const zodExceptionFilter: Middleware = (next) => next() // @filename: main.ts // ---cut--- import { weave, resolver, query } from "@gqloom/zod" import * as z from "zod" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" import { outputValidator, zodExceptionFilter } from "./middlewares" const helloResolver = resolver({ hello: query(z.string().min(10)) .input({ name: z.string() }) .use(outputValidator) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) export const schema = weave(helloResolver, zodExceptionFilter) // [!code hl] const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { // eslint-disable-next-line no-console console.info("Server is running on http://localhost:4000/graphql") }) ``` In the code above, we added a `z.string().min(10)` requirement to the output of the `hello` query and added the `outputValidator` middleware to the parser function. We also added a global middleware `ValibotExceptionFilter` to `weave`. #### Result When we make the following query: ```graphql title="GraphQL Query" { hello(name: "W") } ``` A result similar to the following will be given: ::: code-group ```json [valibot] { "errors": [ { "message": "Invalid length: Expected >=10 but received 8", "locations": [ { "line": 2, "column": 3 } ], "path": [ "hello" ], "extensions": { "issues": [ { "kind": "validation", "type": "min_length", "input": "Hello, W", "expected": ">=10", "received": "8", "message": "Invalid length: Expected >=10 but received 8", "requirement": 10 } ] } } ], "data": null } ``` ```json [zod] { "errors": [ { "message": "String must contain at least 10 character(s)", "locations": [ { "line": 2, "column": 3 } ], "path": [ "hello" ], "extensions": { "issues": [ { "code": "too_small", "minimum": 10, "type": "string", "inclusive": true, "exact": false, "message": "String must contain at least 10 character(s)", "path": [] } ] } } ], "data": null } ``` ::: If we adjust the input so that the returned string is the required length: ```graphql [GraphQL Query] { hello(name: "World") } ``` It will get a response with no exceptions: ```json { "data": { "hello": "Hello, World" } } ``` ### Authentication Checking a user's permissions is a common requirement that we can easily implement with middleware. Consider that our user has the roles `“admin”` and `“editor”`, and we want the administrator and editor to have access to their own actions, respectively. First, we implement an `authGuard` middleware that checks the user's role: ```ts twoslash // @filename: context.ts import { createMemoization } from "@gqloom/core/context" export const useUser = createMemoization(()=> ({ name:"", id: 0, roles: [] as ("admin" | "editor")[] })) // @filename: main.ts // ---cut--- import { type Middleware } from "@gqloom/core" import { useUser } from "./context" import { GraphQLError } from "graphql" export function authGuard(role: "admin" | "editor"): Middleware { return async (next) => { const user = await useUser() if (user == null) throw new GraphQLError("Not authenticated") if (!user.roles.includes(role)) throw new GraphQLError("Not authorized") return next() } } ``` In the code above, we declare an `authGuard` middleware that takes a role parameter and returns a middleware function. The middleware function checks that the user is authenticated and has the specified role, and throws a `GraphQLError` exception if the requirements are not satisfied. We can apply different middleware for different resolvers: ::: code-group ```ts twoslash [valibot] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export function authGuard(role: "admin" | "editor"): Middleware { return (next) => next() } // @filename: main.ts // ---cut--- import { resolver, mutation } from "@gqloom/core" import * as v from "valibot" import { authGuard } from "./middlewares" const adminResolver = resolver( { deleteArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("admin")], // [!code hl] } ) const editorResolver = resolver( { createArticle: mutation(v.boolean(), () => true), updateArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("editor")] } // [!code hl] ) ``` ```ts twoslash [zod] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export function authGuard(role: "admin" | "editor"): Middleware { return (next) => next() } // @filename: main.ts // ---cut--- import { resolver, mutation } from "@gqloom/zod" import * as z from "zod" import { authGuard } from "./middlewares" const adminResolver = resolver( { deleteArticle: mutation(z.boolean(), () => true), }, { middlewares: [authGuard("admin")], // [!code hl] } ) const editorResolver = resolver( { createArticle: mutation(z.boolean(), () => true), updateArticle: mutation(z.boolean(), () => true), }, { middlewares: [authGuard("editor")] } // [!code hl] ) ``` ::: In the code above, we have applied the `authGuard` middleware to `AdminResolver` and `EditorResolver` and assigned different roles to them. In this way, only users with the corresponding roles can access the actions within the corresponding resolvers. ### Logging We can also implement logging functionality through middleware. For example, we can create a `logger` middleware to log the execution time of each field parsing function: ```ts twoslash import { type Middleware } from "@gqloom/core" import { useResolverPayload } from "@gqloom/core/context" export const logger: Middleware = async (next) => { const info = useResolverPayload()!.info const start = Date.now() const result = await next() const resolveTime = Date.now() - start console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`) return result } ``` ### Caching We can implement caching functionality through middleware. For example, we can create a `cache` middleware to cache the resolution results of each query: ```ts twoslash import type { Middleware } from "@gqloom/core" /** Simple in-memory cache implementation */ const cacheStore = new Map() export interface CacheOptions { /** * Time to live in milliseconds * @default 60000 */ ttl?: number } export const cache = (options: CacheOptions = {}): Middleware => { const { ttl = 60000 } = options const middleware: Middleware = async ({ next, payload }) => { if (!payload?.info) { return next() } const { fieldName, parentType } = payload.info const args = payload.args || {} const cacheKey = `${parentType.name}.${fieldName}:${JSON.stringify(args)}` const cached = cacheStore.get(cacheKey) if (cached && Date.now() - cached.timestamp < ttl) { return cached.data } const result = await next() cacheStore.set(cacheKey, { data: result, timestamp: Date.now() }) return result } // Only apply cache to queries by default middleware.operations = ["query"] return middleware } ``` ### Modifying Input We can modify the request input through middleware: ::: code-group ```ts twoslash [valibot] const useUser = async () => ({ id: 1 }) // ---cut--- import { mutation, resolver } from "@gqloom/core" import * as v from "valibot" const Post = v.object({ __typename: v.nullish(v.literal("Post")), id: v.number(), title: v.string(), content: v.string(), authorId: v.number(), }) interface IPost extends v.InferOutput {} const posts: IPost[] = [] export const postsResolver = resolver({ createPost: mutation(Post) .input( v.object({ title: v.string(), content: v.string(), authorId: v.number(), }) ) .use(async ({ next, parseInput }) => { // [!code hl] const result = await parseInput.getResult() // [!code hl] result.authorId = (await useUser()).id // [!code hl] parseInput.setResult(result) // [!code hl] return next() // [!code hl] }) .resolve(({ title, content, authorId }) => { const post = { id: Math.random(), title, content, authorId, } posts.push(post) return post }), }) ``` ```ts twoslash [zod] const useUser = async () => ({ id: 1 }) // ---cut--- import { mutation, resolver } from "@gqloom/core" import * as z from "zod" const Post = z.object({ __typename: z.literal("Post").nullish(), id: z.number(), title: z.string(), content: z.string(), authorId: z.number(), }) interface IPost extends z.output {} const posts: IPost[] = [] export const postsResolver = resolver({ createPost: mutation(Post) .input( z.object({ title: z.string(), content: z.string(), authorId: z.number(), }) ) .use(async ({ next, parseInput }) => { // [!code hl] const result = await parseInput.getResult() // [!code hl] result.authorId = (await useUser()).id // [!code hl] parseInput.setResult(result) // [!code hl] return next() // [!code hl] }) .resolve(({ title, content, authorId }) => { const post = { id: Math.random(), title, content, authorId, } posts.push(post) return post }), }) ``` ::: ## Using middleware GQLoom is able to apply middleware in a variety of scopes, including resolver functions, resolver local middleware, and global middleware. ### Resolve function middleware We can use middleware directly in the resolver function by using the `use` method during its construction, for example: ::: code-group ```ts twoslash [valibot] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export const outputValidator: Middleware = (next) => next() // @filename: main.ts // ---cut--- import { resolver, query } from "@gqloom/core" import * as v from "valibot" import { outputValidator } from "./middlewares" const helloResolver = resolver({ hello: query(v.pipe(v.string(), v.minLength(10))) .input({ name: v.string() }) .use(outputValidator) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) ``` ```ts twoslash [zod] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export const outputValidator: Middleware = (next) => next() // @filename: main.ts // ---cut--- import { resolver, query } from "@gqloom/zod" import * as z from "zod" import { outputValidator } from "./middlewares" const helloResolver = resolver({ hello: query(z.string().min(10)) .input({ name: z.string() }) .use(outputValidator) // [!code hl] .resolve(({ name }) => `Hello, ${name}`), }) ``` ::: ### Resolver-scoped middleware We can also apply middleware at the resolver level, so the middleware will take effect for all operations within the resolver. We just need to use the `use` method to add `middlewares` to the resolver: ::: code-group ```ts twoslash [valibot] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export function authGuard(role: "admin" | "editor"): Middleware { return (next) => next() } // @filename: main.ts // ---cut--- import { resolver, mutation } from "@gqloom/core" import * as v from "valibot" import { authGuard } from "./middlewares" const adminResolver = resolver({ deleteArticle: mutation(v.boolean(), () => true), }).use(authGuard("admin")) // [!code hl] const editorResolver = resolver({ createArticle: mutation(v.boolean(), () => true), updateArticle: mutation(v.boolean(), () => true), }).use(authGuard("editor")) // [!code hl] ``` ```ts twoslash [zod] // @filename: middlewares.ts import { type Middleware } from "@gqloom/core" export function authGuard(role: "admin" | "editor"): Middleware { return (next) => next() } // @filename: main.ts // ---cut--- import { resolver, mutation } from "@gqloom/zod" import * as z from "zod" import { authGuard } from "./middlewares" const adminResolver = resolver({ deleteArticle: mutation(z.boolean(), () => true), }).use(authGuard("admin")) // [!code hl] const editorResolver = resolver({ createArticle: mutation(z.boolean(), () => true), updateArticle: mutation(z.boolean(), () => true), }).use(authGuard("editor")) // [!code hl] ``` ::: ### Global middleware In order to apply global middleware, we need to pass in the middleware fields in the `weave` function, for example: ```ts import { weave } from "@gqloom/core" import { exceptionFilter } from "./middlewares" export const schema = weave(helloResolver, exceptionFilter) // [!code hl] ``` ### Applying Middleware Based on Operation Type We can specify for which operation types a middleware should take effect. ```ts twoslash const db = {} as { beginTransaction: () => Promise commit: () => Promise rollback: () => Promise } // ---cut--- import type { Middleware } from "@gqloom/core" import { GraphQLError } from "graphql" export const transaction: Middleware = async ({ next }) => { try { await db.beginTransaction() const result = await next() await db.commit() return result } catch (error) { await db.rollback() throw new GraphQLError("Transaction failed", { extensions: { originalError: error }, }) } } transaction.operations = ["mutation"] ``` `Middleware.operations` is an array of strings used to specify on which operation types the middleware should take effect. The available values are: * `"query"`; * `"mutation"`; * `"subscription"`; * `"field"`; * `"subscription.resolve"`; * `"subscription.subscribe"`; The default value for `Middleware.operations` is `["field", "query", "mutation", "subscription.subscribe"]`. --- --- url: /docs/schema/valibot.md --- # Valibot [Valibot](https://valibot.dev/) is the open source schema library for TypeScript with bundle size, type safety and developer experience in mind. `@gqloom/valibot` provides integration of GQLoom with Valibot to weave Valibot Schema into GraphQL Schema. ## Installation ::: code-group ```sh [npm] npm i graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [pnpm] pnpm add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [yarn] yarn add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [bun] bun add graphql @gqloom/core valibot @gqloom/valibot ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:valibot npm:@gqloom/valibot ``` ::: ## Defining simple scalars In GQLoom, you can directly use Valibot schemas as [silk](../silk): ```ts twoslash import * as v from "valibot" const StringScalar = v.string() // GraphQLString const BooleanScalar = v.boolean() // GraphQLBoolean const FloatScalar = v.number() // GraphQLFloat const IntScalar = v.pipe(v.number(), v.integer()) // GraphQLInt ``` ## Weave To ensure that `GQLoom` correctly weaves Valibot schemas into the GraphQL schema, we need to add the `ValibotWeaver` from `@gqloom/valibot` when using the `weave` function. ```ts twoslash import { weave, resolver, query } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" export const helloResolver = resolver({ hello: query(v.string(), () => "Hello, World!"), }) export const schema = weave(ValibotWeaver, helloResolver) ``` ## Defining objects We can use Valibot to define objects and use them as [silk](../silk): ```ts twoslash 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()), }) ``` ## Names and more metadata ### Defining names for objects ::: info 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. ::: The recommended practice is to use the `__typename` literal to define a name for the object, for example: ```ts twoslash 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()), }) ``` We can also use the required `__typename` literal to set a specific value, which is very useful when using GraphQL `interface` and `union`, for example: ```ts twoslash 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()), }) ``` ::: details 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. ```ts twoslash import * as v from "valibot" import { collectNames } from "@gqloom/core" export const Cat = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }) collectNames({ Cat }) // Collect names for Cat, which will be presented in the GraphQL Schema after weaving. ``` We can also use the `collectNames` function to define names for objects and deconstruct the returned objects into `Cat` and export them. ```ts twoslash import * as v from "valibot" import { collectNames } from "@gqloom/core" export const { Cat } = collectNames({ Cat: v.object({ name: v.string(), age: v.pipe(v.number(), v.integer()), loveFish: v.nullish(v.boolean()), }), }) ``` ::: ::: details Using `asObjectType` We can use the `asObjectType` function to create metadata and pass it into the `Valibot` pipeline to define a name for the object. The `asObjectType` function takes the complete GraphQL object type definition and returns metadata. ```ts twoslash 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" }) ) ``` ::: ### Adding more metadata With the `asObjectType` function, we can add more metadata to the object, such as `description`, `deprecationReason`, `extensions` and so on. ```ts twoslash 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({ // [!code highlight] name: "Cat", // [!code highlight] description: "A cute cat", // [!code highlight] }) // [!code highlight] ) ``` In the above code, we have added a `description` metadata to the `Cat` object which will be presented in the GraphQL Schema: ```graphql title="GraphQL Schema" """A cute cat""" type Cat { name: String! age: Int! loveFish: Boolean } ``` We can also use the `asField` function to add metadata to a field, such as `description`, `type`, and so on. ```ts twoslash 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({ // [!code highlight] type: GraphQLInt, // [!code highlight] description: "How old is the cat", // [!code highlight] extensions: { // [!code highlight] complexity: 2, // [!code highlight] }, // [!code highlight] }) // [!code highlight] ), loveFish: v.nullish(v.boolean()), }), asObjectType({ name: "Cat", description: "A cute cat", }) ) ``` In the above code, we added `type` and `description` metadata to the `age` field and ended up with the following GraphQL Schema: ```graphql title="GraphQL Schema" """A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean } ``` #### Declaring Interfaces We can use the `asObjectType` function to define interfaces, for example: ```ts twoslash import * as v from "valibot" import { asObjectType } from "@gqloom/valibot" 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] }) ) ``` In the above code, we have created an interface `Fruit` using the `asObjectType` function and declared the `Orange` object as an implementation of the `Fruit` interface using the `interfaces` option. #### Omitting Fields We can also omit fields by setting `type` to `null` using the `asField` function, for example: ```ts twoslash 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 })), }) ``` The following GraphQL Schema will be obtained: ```graphql title="GraphQL Schema" type Dog { name: String } ``` ## Defining Union Types When using `Valibot`, we can define union types using `variant` or `union`. #### Using variant We recommend using `variant` to define union types: ```ts twoslash 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" }) ) ``` In the above code, we have created a union type using the `variant` function. In the case of `Animal`, it distinguishes the specific type by the `__typename` field. #### Using union We can also use `union` to define union types: ```ts twoslash import { collectNames } from "@gqloom/core" import { asUnionType } from "@gqloom/valibot" import * as v from "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 }) ``` In the above code, we have created a union type using the `union` function. In the case of `Animal`, it uses the `resolveType` function to differentiate between specific types. Here, if an animal likes fish, then it is a cat, otherwise it is a dog. ## Defining Enumeration Types We can define enumeration types using `v.picklist` or `v.enum_`. #### Using picklist In general, we prefer to use `v.picklist` to define enumerated types: ```ts twoslash import * as v from "valibot" import { asEnumType } 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 ``` #### Using enum\_ We can also use `v.enum_` to define enumeration types: ```ts twoslash import { asEnumType } from "@gqloom/valibot" import * as v from "valibot" 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" }, }, }) ) ``` ## Custom Type Mappings To accommodate more Valibot types, we can extend GQLoom to add more type mappings. First we use `ValibotWeaver.config` to define the type mapping configuration.\ Here we import the `GraphQLDateTime`, `GraphQLJSON` and `GraphQLJSONObject` scalars from \[graphql-scalars] and map them to the matching GraphQL scalars when encountering the `date`, `any` and `record` types. ```ts twoslash 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 } }, }) ``` Configurations are passed into the `weave` function when weaving the GraphQL Schema: ```ts twoslash import { GraphQLDateTime, GraphQLJSON, GraphQLJSONObject, } from "graphql-scalars" import { resolver } from "@gqloom/core" 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 } }, }) const helloResolver = resolver({}) // ---cut--- import { weave } from "@gqloom/core" export const schema = weave(valibotWeaverConfig, helloResolver) ``` ## Default Type Mappings The following table lists the default mappings between Valibot types and GraphQL types in GQLoom: | Valibot types | GraphQL types | | --------------------------------- | ------------------- | | `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.nanoid())` | `GraphQLID` | | `v.pipe(v.string(), v.ulid())` | `GraphQLID` | | `v.pipe(v.string(), v.uuid())` | `GraphQLID` | | `v.union()` | `GraphQLUnionType` | | `v.variant()` | `GraphQLUnionType` | --- --- url: /docs/schema/zod.md --- # Zod [Zod](https://zod.dev/) is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object. Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures. `@gqloom/zod` provides integration of GQLoom with Zod to weave Zod Schema into GraphQL Schema. ## Installation ::: code-group ```sh [npm] npm i graphql @gqloom/core zod @gqloom/zod ``` ```sh [pnpm] pnpm add graphql @gqloom/core zod @gqloom/zod ``` ```sh [yarn] yarn add graphql @gqloom/core zod @gqloom/zod ``` ```sh [bun] bun add graphql @gqloom/core zod @gqloom/zod ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:zod npm:@gqloom/zod ``` ::: ## Defining simple scalars In GQLoom, you can directly use Zod Schema as [silk](../silk). ```ts twoslash import * as z from "zod" const StringScalar = z.string() // GraphQLString const BooleanScalar = z.boolean() // GraphQLBoolean const FloatScalar = z.number() // GraphQLFloat const IntScalar = z.int() // GraphQLInt ``` ## Weave To ensure that `GQLoom` correctly weaves the Zod Schema into the GraphQL Schema, we need to add the `ZodWeaver` from `@gqloom/zod` when using the `weave` function. ```ts twoslash import { ZodWeaver, weave, resolver, query } from "@gqloom/zod" import * as z from "zod" export const helloResolver = resolver({ hello: query(z.string(), () => "Hello, World!"), }) export const schema = weave(ZodWeaver, helloResolver) ``` ## Defining Objects We can define objects using Zod and use them as [silk](../silk) to use: ```ts twoslash import * as z from "zod" export const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) ``` ## Names and more metadata ### Defining names for objects ::: info 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. ::: The recommended practice is to use the `__typename` literal to define a name for the object, for example: ```ts twoslash import * as z from "zod" export const Cat = z.object({ __typename: z.literal("Cat").nullish(), name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) ``` We can also use the required `__typename` literal to set a specific value, which is very useful when using GraphQL `interface` and `union`, for example: ```ts twoslash import * as z from "zod" export const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) ``` ::: details Using `collectNames` ```ts twoslash import * as z from "zod" import { collectNames } from "@gqloom/core" export const Cat = z.object({ name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) collectNames({ Cat }) ``` We can also use the `collectNames` function to define names for objects and deconstruct the returned objects into `Cat` and export them. ```ts twoslash import * as z from "zod" import { collectNames } from "@gqloom/core" export const { Cat } = collectNames({ Cat: z.object({ name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }), }) ``` ::: ::: details Using `asObjectType` We can use the `asObjectType` function to create metadata and pass it into the `Zod` pipeline to define a name for the object. The `asObjectType` function takes the complete GraphQL object type definition and returns metadata. ```ts twoslash import { asObjectType } from "@gqloom/zod" import * as z from "zod/v4" export const Cat = z .object({ name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) .register(asObjectType, { name: "Cat" }) ``` ```ts twoslash import { asObjectType } from "@gqloom/zod/v3" import * as z from "zod/v3" export const Cat = z .object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) .superRefine(asObjectType({ name: "Cat" })) ``` ::: ### Adding more metadata With the `asObjectType` register, we can add more metadata to the object, such as `description`, `deprecationReason`, `extensions` and so on. ```ts twoslash import { asObjectType } from "@gqloom/zod" import * as z from "zod/v4" export const Cat = z .object({ name: z.string(), age: z.int(), loveFish: z.boolean().nullish(), }) .register(asObjectType, { name: "Cat", description: "A cute cat", }) ``` ```ts twoslash import { asObjectType } from "@gqloom/zod/v3" import * as z from "zod/v3" export const Cat = z .object({ name: z.string(), age: z.number().int(), loveFish: z.boolean().nullish(), }) .superRefine( asObjectType({ name: "Cat", description: "A cute cat", }) ) ``` In the above code, we have added a `description` metadata to the `Cat` object which will be presented in the GraphQL Schema: ```graphql title="GraphQL Schema" """A cute cat""" type Cat { name: String! age: Int! loveFish: Boolean } ``` We can also use the asField function to add metadata to a field, such as description, type, and so on. ```ts twoslash import { asField, asObjectType } from "@gqloom/zod" import { GraphQLInt } from "graphql" import * as z from "zod/v4" export const Cat = z .object({ name: z.string(), age: z.number().register(asField, { // [!code highlight] type: GraphQLInt, // [!code highlight] description: "How old is the cat", // [!code highlight] extensions: { // [!code highlight] complexity: 2, // [!code highlight] }, // [!code highlight] }), // [!code highlight] loveFish: z.boolean().nullish(), }) .register(asObjectType, { name: "Cat", description: "A cute cat", }) ``` ```ts twoslash import { asField, asObjectType } from "@gqloom/zod/v3" import { GraphQLInt } from "graphql" import * as z from "zod/v3" export const Cat = z .object({ name: z.string(), age: z .number() .superRefine( asField({ // [!code highlight] type: GraphQLInt, // [!code highlight] description: "How old is the cat", // [!code highlight] extensions: { // [!code highlight] complexity: 2, // [!code highlight] }, // [!code highlight] }) ), loveFish: z.boolean().nullish(), }) .superRefine( asObjectType({ name: "Cat", description: "A cute cat", }) ) ``` In the above code, we added `type` and `description` metadata to the `age` field and ended up with the following GraphQL Schema: ```graphql title="GraphQL Schema" """A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean } ``` #### Declaring Interfaces We can also use the `asObjectType` function to declare interfaces, for example: ```ts twoslash import * as z from "zod/v4" import { asObjectType } from "@gqloom/zod" const Fruit = z .object({ __typename: z.literal("Fruit").nullish(), name: z.string(), color: z.string(), prize: z.number(), }) .describe("Some fruits you might like") const Orange = z .object({ name: z.string(), color: z.string(), prize: z.number(), }) .register(asObjectType, { name: "Orange", interfaces: [Fruit] }) ``` ```ts twoslash import { asObjectType } from "@gqloom/zod/v3" import * as z from "zod/v3" const Fruit = z .object({ __typename: z.literal("Fruit").nullish(), name: z.string(), color: z.string(), prize: z.number(), }) .describe("Some fruits you might like") const Orange = z .object({ name: z.string(), color: z.string(), prize: z.number(), }) .superRefine(asObjectType({ name: "Orange", interfaces: [Fruit] })) ``` In the above code, we created an interface `Fruit` using the `asObjectType` function and declared the `Orange` object as an implementation of the `Fruit` interface using the `interfaces` option. #### Omitting Fields We can also omit fields by setting `type` to `null` using the `asField` function, for example: ```ts twoslash import { asField } from "@gqloom/zod" import * as z from "zod/v4" const Dog = z.object({ __typename: z.literal("Dog").nullish(), name: z.string().nullish(), birthday: z.date().nullish().register(asField, { type: null }), }) ``` ```ts twoslash import { asField } from "@gqloom/zod/v3" import * as z from "zod/v3" const Dog = z.object({ __typename: z.literal("Dog").nullish(), name: z.string().nullish(), birthday: z .date() .nullish() .superRefine(asField({ type: null })), }) ``` The following GraphQL Schema will be generated: ```graphql title="GraphQL Schema" type Dog { name: String } ``` ### Using Zod native metadata Zod v4 provides native metadata support. GQLoom reads metadata from `.meta()` or the global registry, so you can rely less on GQLoom-specific registries. #### Auto-mapping rules By default, GQLoom maps Zod's `GlobalMeta` as follows: * `title` → GraphQL type or enum name. * `description` → description (takes precedence over `.describe()`). ```ts twoslash import * as z from "zod/v4" export const User = z .object({ id: z.string().meta({ description: "User unique identifier" }), name: z.string(), }) .meta({ title: "User", description: "User information in the system", }) ``` ::: tip Priority GQLoom reads metadata in this order (highest first): 1. Config from `asObjectType` / `asField` and other registries. 2. Custom `metaTo*Config` in `ZodWeaver.config`. 3. Zod native metadata (`globalRegistry` over `.meta()`). 4. Zod `.describe()` description. ::: #### Using the global registry You can use Zod v4's `globalRegistry` to attach or inject metadata for existing schemas in bulk or from outside: ```ts twoslash import * as z from "zod/v4" import { globalRegistry } from "zod/v4/core" export const User = z.object({ id: z.string(), name: z.string(), }) // Inject GraphQL metadata for User from outside globalRegistry.add(User, { title: "User", description: "Description injected via global registry", }) ``` #### Advanced: custom mapping To use custom metadata fields or control how metadata becomes GraphQL config, configure the hooks in `ZodWeaver.config`: ```ts twoslash import { ZodWeaver } from "@gqloom/zod" export const zodWeaverConfig = ZodWeaver.config({ // Map Zod metadata deprecated to GraphQL deprecation reason metaToFieldConfig: (meta) => ({ description: meta.description, deprecationReason: meta.deprecated ? "This field is deprecated, please migrate to the new field" : undefined, }), }) ``` Supported hooks: `metaToObjectConfig`, `metaToFieldConfig`, `metaToEnumConfig`, and `metaToUnionConfig`. ## Defining Union Types #### Using z.discriminatedUnion We recommend using `z.discriminatedUnion` to define union types, for example: ```ts twoslash import { asUnionType } from "@gqloom/zod" import { z } from "zod/v4" const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ __typename: z.literal("Dog"), name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z .discriminatedUnion("__typename", [Cat, Dog]) .register(asUnionType, { name: "Animal" }) ``` ```ts twoslash import { asUnionType } from "@gqloom/zod/v3" import * as z from "zod/v3" const Cat = z.object({ __typename: z.literal("Cat"), name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ __typename: z.literal("Dog"), name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z .discriminatedUnion("__typename", [Cat, Dog]) .superRefine(asUnionType("Animal")) ``` In the above code, we have created a union type using the `z.discriminatedUnion` function. In the case of `Animal`, it distinguishes the specific type by the `__typename` field. #### Using z.union We can also use `z.union` to define union types: ```ts twoslash import { z } from "zod/v4" import { collectNames } from "@gqloom/core" import { asUnionType } from "@gqloom/zod" const Cat = z.object({ name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z.union([Cat, Dog]).register(asUnionType, { name: "Animal", resolveType: (it) => (it.loveFish ? "Cat" : "Dog"), }) collectNames({ Cat, Dog, Animal }) ``` ```ts twoslash import { collectNames } from "@gqloom/core" import { asUnionType } from "@gqloom/zod/v3" import * as z from "zod/v3" const Cat = z.object({ name: z.string(), age: z.number(), loveFish: z.boolean().optional(), }) const Dog = z.object({ name: z.string(), age: z.number(), loveBone: z.boolean().optional(), }) const Animal = z.union([Cat, Dog]).superRefine( asUnionType({ name: "Animal", resolveType: (it) => (it.loveFish ? "Cat" : "Dog"), }) ) collectNames({ Cat, Dog, Animal }) ``` In the above code, we have created a union type using the `z.union` function. For `Animal`, we use the `resolveType` function to differentiate between specific types. Here, if an animal likes fish, then it is a cat, otherwise it is a dog. ## Defining Enumeration Types We can define enum types using `z.enum` or `z.nativeEnum`. #### Using z.enum In general, we prefer to use `z.enum` to define enumeration types, for example: ```ts twoslash import { asEnumType } from "@gqloom/zod" import * as z from "zod/v4" export const Fruit = z .enum(["apple", "banana", "orange"]) .register(asEnumType, { name: "Fruit", valuesConfig: { apple: { description: "red" }, banana: { description: "yellow" }, orange: { description: "orange" }, }, }) export type IFruit = z.infer ``` ```ts twoslash import { asEnumType } from "@gqloom/zod/v3" import * as z from "zod/v3" export const Fruit = z.enum(["apple", "banana", "orange"]).superRefine( asEnumType({ name: "Fruit", valuesConfig: { apple: { description: "red" }, banana: { description: "yellow" }, orange: { description: "orange" }, }, }) ) export type IFruit = z.infer ``` ## Customize Type Mappings To accommodate more Zod types, we can extend GQLoom to add more type mappings to it. First we use `ZodWeaver.config` to define the type mapping configuration.\ Here we import the `GraphQLDateTime`, `GraphQLJSON` and `GraphQLJSONObject` scalars from [graphql-scalars](https://the-guild.dev/graphql/scalars) and map them to the matching GraphQL scalars when encountering the `date`, `any` and `record` types. ```ts twoslash import { GraphQLDateTime, GraphQLJSON, GraphQLJSONObject, } from "graphql-scalars" import * as z from "zod" import { ZodWeaver } from "@gqloom/zod" export const zodWeaverConfig = ZodWeaver.config({ presetGraphQLType: (schema) => { if (schema instanceof z.ZodDate) return GraphQLDateTime if (schema instanceof z.ZodAny) return GraphQLJSON if (schema instanceof z.ZodRecord) return GraphQLJSONObject }, }) ``` Configurations are passed into the `weave` function when weaving the GraphQL Schema: ```ts twoslash import { GraphQLDateTime, GraphQLJSON, GraphQLJSONObject, } from "graphql-scalars" import * as z from "zod" import { resolver } from '@gqloom/core' import { ZodWeaver } from "@gqloom/zod" export const zodWeaverConfig = ZodWeaver.config({ presetGraphQLType: (schema) => { if (schema instanceof z.ZodDate) return GraphQLDateTime if (schema instanceof z.ZodAny) return GraphQLJSON if (schema instanceof z.ZodRecord) return GraphQLJSONObject }, }) export const helloResolver = resolver({}) // ---cut--- import { weave } from "@gqloom/zod" export const schema = weave(zodWeaverConfig, helloResolver) ``` ## Default Type Mappings The following table lists the default mappings between Zod types and GraphQL types in GQLoom: | Zod types | GraphQL types | | ------------------------ | ------------------- | | `z.array()` | `GraphQLList` | | `z.string()` | `GraphQLString` | | `z.string().cuid()` | `GraphQLID` | | `z.string().cuid2()` | `GraphQLID` | | `z.string().ulid()` | `GraphQLID` | | `z.string().uuid()` | `GraphQLID` | | `z.literal("")` | `GraphQLString` | | `z.literal(false)` | `GraphQLBoolean` | | `z.literal(0)` | `GraphQLFloat` | | `z.number()` | `GraphQLFloat` | | `z.int()` | `GraphQLInt` | | `z.boolean()` | `GraphQLBoolean` | | `z.object()` | `GraphQLObjectType` | | `z.enum()` | `GraphQLEnumType` | | `z.nativeEnum()` | `GraphQLEnumType` | | `z.union()` | `GraphQLUnionType` | | `z.discriminatedUnion()` | `GraphQLUnionType` | --- --- url: /docs/schema/json.md --- # JSON Schema [JSON Schema](https://json-schema.org/) is a declarative language for annotating and validating JSON documents' structure, constraints, and data types. It helps you standardize and define expectations for JSON data. ## Installation We can use JSON Schema directly in our project, or use [typebox](https://sinclairzx81.github.io/typebox/) to help us build JSON Schema. ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/json ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/json ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/json ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/json ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/json ``` ::: ::: code-group ```sh [npm] npm i graphql @gqloom/core typebox @gqloom/json ``` ```sh [pnpm] pnpm add graphql @gqloom/core typebox @gqloom/json ``` ```sh [yarn] yarn add graphql @gqloom/core typebox @gqloom/json ``` ```sh [bun] bun add graphql @gqloom/core typebox @gqloom/json ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:typebox npm:@gqloom/json ``` ::: ## Defining Simple Scalars In GQLoom, you can use the `jsonSilk` function to use a JSON Schema as a [silk](../silk.md): ```ts twoslash import { jsonSilk } from "@gqloom/json" const StringScalar = jsonSilk({ type: "string" }) const BooleanScalar = jsonSilk({ type: "boolean" }) const FloatScalar = jsonSilk({ type: "number" }) const IntScalar = jsonSilk({ type: "integer" }) ``` Since `TypeBox` does not adhere to the [Standard Schema](https://github.com/standard-schema/standard-schema), we need to wrap the `TypeBox` schema with an additional function to make it usable in GQLoom: ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } ``` Then, we can use the `typeSilk` function to convert a `typebox` schema into a [silk](../silk.md): ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } // ---cut--- import { Type } from "typebox" const StringScalar = typeSilk(Type.String()) const BooleanScalar = typeSilk(Type.Boolean()) const FloatScalar = typeSilk(Type.Number()) const IntScalar = typeSilk(Type.Integer()) ``` ## Defining Objects When defining an object, you also need to wrap it with the `jsonSilk` function: ```ts twoslash import { jsonSilk } from "@gqloom/json" const Cat = jsonSilk({ title: "Cat", type: "object", properties: { name: { type: "string" }, age: { type: "integer" }, loveFish: { type: ["boolean", "null"] }, }, required: ["name", "age"], }) ``` When defining an object, you also need to wrap it with the `typeSilk` function: ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } // ---cut--- import { Type } from "typebox" const Cat = typeSilk( Type.Object( { name: Type.String(), age: Type.Integer(), loveFish: Type.Optional(Type.Boolean()), }, { title: "Cat" } ) ) ``` ### Naming Objects ::: info 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. ::: We can use the `title` property of JSON Schema to name an object, as in the example code above; we can also use a `__typename` literal to name it: ```ts twoslash import { jsonSilk } from "@gqloom/json" const Cat = jsonSilk({ type: "object", properties: { __typename: { const: "Cat" }, name: { type: "string" }, age: { type: "integer" }, loveFish: { type: ["boolean", "null"] }, }, required: ["name", "age"], }) ``` ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } // ---cut--- import { Type } from "typebox" const Cat = typeSilk( Type.Object({ __typename: Type.Optional(Type.Literal("Cat")), name: Type.String(), age: Type.Integer(), loveFish: Type.Optional( Type.Boolean({ description: "Does the cat love fish?" }) ), }) ) ``` ## Defining Union Types We can use the `oneOf` property of JSON Schema to define a union type: ```ts twoslash import { jsonSilk } from "@gqloom/json" const Cat = jsonSilk({ title: "Cat", type: "object", properties: { __typename: { const: "Cat" }, name: { type: "string" }, loveFish: { type: "boolean" }, }, }) const Dog = jsonSilk({ title: "Dog", type: "object", properties: { __typename: { const: "Dog" }, name: { type: "string" }, loveBone: { type: "boolean" }, }, }) const Animal = jsonSilk({ title: "Animal", oneOf: [Cat, Dog], }) ``` We can use the `Type.Union` function to define a union type: ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } // ---cut--- import { Type } from "typebox" const Cat = typeSilk( Type.Object( { __typename: Type.Literal("Cat"), name: Type.String(), loveFish: Type.Boolean(), }, { title: "Cat" } ) ) const Dog = typeSilk( Type.Object( { __typename: Type.Literal("Dog"), name: Type.String(), loveBone: Type.Boolean(), }, { title: "Dog" } ) ) const Animal = typeSilk(Type.Union([Cat, Dog], { title: "Animal" })) ``` ## Defining Enum Types We can use the `enum` property of JSON Schema to define an enum type: ```ts twoslash import { jsonSilk } from "@gqloom/json" const Fruit = jsonSilk({ title: "Fruit", description: "Some fruits you might like", enum: ["apple", "banana", "orange"], }) ``` We can use the `Type.Enum` function to define an enum type: ```ts twoslash import { type TSchema, type Static } from "typebox" import { type GraphQLSilk } from "@gqloom/core" import { JSONWeaver } from "@gqloom/json" export function typeSilk( type: T ): T & GraphQLSilk, Static> { return JSONWeaver.unravel(type) as T & GraphQLSilk, Static> } // ---cut--- import { Type } from "typebox" const Fruit = typeSilk( Type.Enum(["apple", "banana", "orange"], { title: "Fruit", description: "Some fruits you might like", }) ) ``` ## Custom Type Mapping To accommodate more JSON Schema types, we can extend GQLoom by adding more type mappings. First, we use `JSONWeaver.config` to define the configuration for type mapping. Here we import the `GraphQLDate` scalar from [graphql-scalars](https://the-guild.dev/graphql/scalars). When a `date` type is encountered, we map it to the corresponding GraphQL scalar. ```ts twoslash import { JSONWeaver } from "@gqloom/json" import { GraphQLDate } from "graphql-scalars" const jsonWeaverConfig = JSONWeaver.config({ presetGraphQLType: (schema) => { if (typeof schema === "object" && schema.format === "date") return GraphQLDate }, }) ``` When weaving the GraphQL Schema, pass the configuration to the `weave` function: ```ts import { weave } from "@gqloom/json" export const schema = weave(jsonWeaverConfig, helloResolver) ``` ## Default Type Mappings The following table lists the default mappings between JSON Schema types and GraphQL types in GQLoom: | JSON Schema Property | GraphQL Type | | --------------------- | ------------------- | | `{"type": "string"}` | `GraphQLString` | | `{"type": "number"}` | `GraphQLFloat` | | `{"type": "integer"}` | `GraphQLInt` | | `{"type": "boolean"}` | `GraphQLBoolean` | | `{"type": "object"}` | `GraphQLObjectType` | | `{"type": "array"}` | `GraphQLList` | | `{"enum": [...]}` | `GraphQLEnumType` | | `{"oneOf": [...]}` | `GraphQLUnionType` | | `{"anyOf": [...]}` | `GraphQLUnionType` | --- --- url: /docs/schema/yup.md --- # Yup [Yup](https://github.com/jquense/yup) is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformation. `@gqloom/yup` provides integration of GQLoom with Yup to weave Yup Schema into GraphQL Schema. ## Installation ::: code-group ```sh [npm] npm i graphql @gqloom/core yup @gqloom/yup ``` ```sh [pnpm] pnpm add graphql @gqloom/core yup @gqloom/yup ``` ```sh [yarn] yarn add graphql @gqloom/core yup @gqloom/yup ``` ```sh [bun] bun add graphql @gqloom/core yup @gqloom/yup ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:yup npm:@gqloom/yup ``` ::: Additionally, we need to declare GQLoom metadata for Yup in the project: ```ts [yup.d.ts] import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } ``` Also, we need to declare metadata from GQLoom for Yup in the project: ```ts [yup.d.ts] import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } ``` ## Defining simple scalars Yup Schema can be used directly as a [silk](../silk) in GQLoom: ```ts twoslash import { number, string, boolean } from "yup" const StringScalar = string() // GraphQLString const BooleanScalar = boolean() // GraphQLBoolean const FloatScalar = number() // GraphQLFloat const IntScalar = number().integer() // GraphQLInt ``` ## Weaving For `GQLoom` to properly weave Yup Schema into GraphQL Schema, we need to add `YupWeaver` from `@gqloom/yup` when using the `weave` function. ```ts twoslash import { weave, resolver, query } from "@gqloom/core" import { YupWeaver } from "@gqloom/yup" import { string } from "yup" export const helloResolver = resolver({ hello: query(string(), () => "Hello, World!"), }) export const schema = weave(YupWeaver, helloResolver) ``` ## Defining objects We can use Yup to define objects and use them as [silk](../silk) to use: ```ts twoslash import { string, boolean, object, number } from "yup" export const Cat = object({ name: string().required(), age: number().integer().required(), loveFish: boolean(), }).label("Cat") ``` ## Names and more metadata ### Defining names for objects ::: info 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. ::: #### Using `label()` The recommended practice is to use the built-in `label` method in `yup` to define a name for the object, for example: ```ts twoslash import { string, boolean, object, number } from "yup" export const Cat = object({ name: string().required(), age: number().integer().required(), loveFish: boolean(), }).label("Cat") ``` ::: details 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. ```ts twoslash import { string, boolean, object, number } from "yup" import { collectNames } from "@gqloom/yup" export const Cat = object({ name: string().required(), age: number().integer().required(), loveFish: 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. ```ts twoslash import { string, boolean, object, number } from "yup" import { collectNames } from "@gqloom/yup" export const { Cat } = collectNames({ Cat: object({ name: string().required(), age: number().integer().required(), loveFish: boolean(), }), }) ``` ::: ::: details Using `asObjectType` metadata We can use the `meta` function in Yup Schema to define a name for the object.\ Here, we define the `asObjectType` metadata and set it to `{ name: "Cat" }` so that in the generated GraphQL Schema, the object will have the name `Cat`. ```ts twoslash import { string, boolean, object, number } from "yup" export const Cat = object({ name: string().required(), age: number().integer().required(), loveFish: boolean(), }).meta({ asObjectType: { name: "Cat" } }) ``` ::: ### Adding more metadata We can use the `meta` function in Yup Schema to add more metadata, such as `description`, `deprecationReason`, `extensions` and so on. ```ts twoslash import { string, boolean, object, number } from "yup" export const Cat = object({ name: string().required(), age: number().integer().required(), loveFish: boolean(), }).meta({ asObjectType: { name: "Cat", description: "A cute cat" } }) ``` In the above code, we have added `description` metadata to the `Cat` object so that in the generated GraphQL Schema, the object will have the description `A cute cat`: ```graphql [GraphQL Schema] """A cute cat""" type Cat { name: String! age: Int! loveFish: Boolean } ``` We can also use the `asField` attribute in the metadata to add metadata to the field, such as `description`, `type`, and so on: ```ts twoslash import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } // ---cut--- import { string, boolean, object, number } from "yup" import { GraphQLInt } from "graphql" export const Cat = object({ name: string().required(), age: number().meta({ asField: { // [!code highlight] type: () => GraphQLInt, // [!code highlight] description: "How old is the cat", // [!code highlight] extensions: { // [!code highlight] complexity: 2, // [!code highlight] }, // [!code highlight] }, // [!code highlight] }), loveFish: boolean(), }).meta({ asObjectType: { name: "Cat", description: "A cute cat" } }) ``` In the above code, we added `type` and `description` metadata to the `age` field and ended up with the following GraphQL Schema: ```graphql [GraphQL Schema] """A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean } ``` #### Declaring Interfaces We can also use the asObjectType function to declare interfaces, for example: ```ts twoslash import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } // ---cut--- import { string, object, number } from "yup" const Fruit = object({ name: string().required(), color: string().required(), prize: number() .required() .meta({ description: "How much do you want to win?" }), }) .meta({ description: "Fruit Interface" }) .label("Fruit") const Orange = object({ name: string().required(), color: string().required(), prize: number().required(), }) .meta({ asObjectType: { interfaces: [Fruit] } }) .label("Orange") ``` In the code above, we declared the `Orange` object as an implementation of the `Fruit` interface using the `interfaces` attribute in `asObjectType`. #### Omitting fields We can also omit fields by setting the type to null using the asField attribute, for example: ```ts twoslash import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } // ---cut--- import { string, boolean, object, number } from "yup" export const Cat = object({ name: string().required(), age: number() .integer() .meta({ asField: { type: null } }), }).meta({ asObjectType: { name: "Cat", description: "A cute cat" } }) ``` The following GraphQL Schema will be generated: ```graphql [GraphQL Schema] type Dog { name: String } ``` ## Defining Union Types Use `union` from `@gqloom/yup` to define union types, for example: ```ts import { object, string, number } from "yup" import { union } from "@gqloom/yup" const Cat = object({ name: string(). required(), color: string().required(), }).label("Cat") const Dog = object({ name: string().required(), height: number().required(), }).label("Dog") const Animal = union([Cat, Dog]).label("Animal") ``` In the above code, we used the `union` function to define the `Cat` and `Dog` objects as members of the `Animal` union type. ## Defining enumerated types #### Using `oneof()` We can use `string().oneof()` to define enumerated types, for example: ```ts twoslash import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } // ---cut--- import { string } from "yup" const Fruit = string() .oneOf(["apple", "banana", "orange"]) .label("Fruit") .meta({ asEnumType: { description: "Some fruits you might like", valuesConfig: { apple: { description: "Apple is red" }, banana: { description: "Banana is yellow" }, orange: { description: "Orange is orange" }, }, }, }) ``` #### Using `enum` We can also use `enum` to define enumeration types, for example: ```ts twoslash import 'yup' import { type GQLoomMetadata } from "@gqloom/yup" declare module "yup" { export interface CustomSchemaMetadata extends GQLoomMetadata {} } // ---cut--- import { mixed } from "yup" enum FruitEnum { apple, banana, orange, } const Fruit = mixed() .oneOf(Object.values(FruitEnum) as FruitEnum[]) .label("Fruit") .meta({ asEnumType: { enum: FruitEnum, description: "Some fruits you might like", valuesConfig: { apple: { description: "Apple is red" }, banana: { description: "Banana is yellow" }, orange: { description: "Orange is orange" }, }, }, }) ``` ## Custom Type Mappings To accommodate more Yup types, we can extend GQLoom to add more type mappings to it. First we use `YupWeaver.config` to define the type mapping configuration.\ Here we import `GraphQLDateTime` from [graphql-scalars](https://the-guild.dev/graphql/scalars), and when we encounter a `date` type, we map it to the matching GraphQL scalar. ```ts twoslash import { GraphQLDateTime } from "graphql-scalars" import { YupWeaver } from "@gqloom/yup" export const yupWeaverConfig = YupWeaver.config({ presetGraphQLType: (description) => { switch (description.type) { case "date": return GraphQLDateTime } }, }) ``` Configurations are passed into the weave function when weaving the GraphQL Schema: ```ts twoslash import { GraphQLDateTime } from "graphql-scalars" import { resolver } from "@gqloom/core" import { YupWeaver } from "@gqloom/yup" export const yupWeaverConfig = YupWeaver.config({ presetGraphQLType: (description) => { switch (description.type) { case "date": return GraphQLDateTime } }, }) export const helloResolver = resolver({}) // ---cut--- import { weave } from "@gqloom/yup" export const schema = weave(yupWeaverConfig, helloResolver) ``` ## Default Type Mappings The following table lists the default mappings between Yup types and GraphQL types in GQLoom: | Yup types | GraphQL types | | ---------------------------- | ------------------- | | `string()` | `GraphQLString` | | `number()` | `GraphQLFloat` | | `number().integer()` | `GraphQLInt` | | `boolean()` | `GraphQLBoolean` | | `object()` | `GraphQLObjectType` | | `array()` | `GraphQLList` | | `union()` | `GraphQLUnionType` | | `string().oneof(["Value1"])` | `GraphQLEnumType` | --- --- url: /docs/schema/effect.md --- # Effect [Effect](https://effect.website/docs/essentials/Schema)'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 ::: code-group ```sh [npm] npm i graphql @gqloom/core effect @gqloom/effect ``` ```sh [pnpm] pnpm add graphql @gqloom/core effect @gqloom/effect ``` ```sh [yarn] yarn add graphql @gqloom/core effect @gqloom/effect ``` ```sh [bun] bun add graphql @gqloom/core effect @gqloom/effect ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:effect npm:@gqloom/effect ``` ::: ## Defining simple scalars In GQLoom, you can directly use Effect Schema as [silk](../silk): ```ts twoslash 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" })) // GraphQLID ``` ## Weave Use `EffectWeaver` to let GQLoom understand Effect Schema: ```ts twoslash 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 ```ts twoslash 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 ::: info 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: ```ts twoslash 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: ```ts twoslash 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), }) ``` ::: details 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. ```ts twoslash 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. ```ts twoslash 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 ```ts twoslash 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]: { // [!code highlight] type: GraphQLInt, // [!code highlight] description: "How old is the cat", // [!code highlight] extensions: { // [!code highlight] complexity: 2, // [!code highlight] }, // [!code highlight] }, // [!code highlight] }), loveFish: Schema.NullOr(Schema.Boolean), }).annotations({ [asObjectType]: { name: "Cat", description: "A cute cat" }, }) ``` The generated GraphQL Schema: ```graphql title="GraphQL Schema" """A cute cat""" type Cat { name: String! """How old is the cat""" age: Int loveFish: Boolean } ``` ### Declaring interfaces ```ts twoslash 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: ```ts twoslash 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: ```ts twoslash 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: ```ts twoslash 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: ```ts twoslash 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( 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 | --- --- url: /docs/schema/mikro-orm.md --- # MikroORM [MikroORM](https://mikro-orm.io/) is a TypeScript ORM for Node.js that supports PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, and more. It is based on the Data Mapper, Unit of Work, and Identity Map patterns, aiming to provide a powerful and easy-to-use database toolset. `@gqloom/mikro-orm` provides integration between GQLoom and MikroORM: * Use MikroORM Entities as [Silks](../silk); * Use resolver factories to quickly generate CRUD operations from MikroORM. ## Installation Please refer to MikroORM's [Quick Start guide](https://mikro-orm.io/docs/quick-start) to install MikroORM and the corresponding database driver. After completing the MikroORM installation, install `@gqloom/mikro-orm`: ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/mikro-orm ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/mikro-orm ``` ::: ## Using Silks Wrap MikroORM Entities with `mikroSilk` to use them as [Silks](../silk). There are two ways to define entities: When using `defineEntity` to define entities, wrap them with `mikroSilk`. Before using them in resolvers you need to initialize MikroORM; example below: ::: code-group ```ts twoslash [entities.ts] import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const User = mikroSilk(UserEntity) export const Post = mikroSilk(PostEntity) ``` ```ts twoslash [provider.ts] // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const User = mikroSilk(UserEntity) export const Post = mikroSilk(PostEntity) // @filename: provider.ts // ---cut--- import { createMemoization, useResolvingFields } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) export const useSelectedFields = () => { return Array.from(useResolvingFields()?.selectedFields ?? ["*"]) as [] } ``` ```ts twoslash [index.ts] // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const User = mikroSilk(UserEntity) export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { weave } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import { Post, User } from "./entities" import { useEm } from "./provider" const userResolver = new MikroResolverFactory(User, useEm).resolver() const postResolver = new MikroResolverFactory(Post, useEm).resolver() export const schema = weave(userResolver, postResolver) ``` ::: When using decorators to define entities, wrap the entity class with `mikroSilk`. Because class entities are resolved from MikroORM metadata, provide the metadata store via `MikroWeaver.config` when weaving. ::: warning Note When using decorators, ensure you import `reflect-metadata` at the top of your app entry and install that dependency. ::: ::: code-group ```ts twoslash [entities.ts] import "reflect-metadata" import { mikroSilk } from "@gqloom/mikro-orm" import { Collection } from "@mikro-orm/core" import { Entity, ManyToOne, OneToMany, PrimaryKey, Property } from "@mikro-orm/decorators/legacy" @Entity() export class AuthorEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public name!: string @OneToMany(() => BookEntity, (b: BookEntity) => b.author) public books = new Collection(this) } @Entity() export class BookEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public title!: string @ManyToOne(() => AuthorEntity, { ref: true }) public author!: AuthorEntity } export const Author = mikroSilk(AuthorEntity) export const Book = mikroSilk(BookEntity) ``` ```ts twoslash [provider.ts] // @filename: entities.ts import "reflect-metadata" import { mikroSilk } from "@gqloom/mikro-orm" import { Collection } from "@mikro-orm/core" import { Entity, ManyToOne, OneToMany, PrimaryKey, Property } from "@mikro-orm/decorators/legacy" @Entity() export class AuthorEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public name!: string @OneToMany(() => BookEntity, (b: BookEntity) => b.author) public books = new Collection(this) } @Entity() export class BookEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public title!: string @ManyToOne(() => AuthorEntity, { ref: true }) public author!: AuthorEntity } export const Author = mikroSilk(AuthorEntity) export const Book = mikroSilk(BookEntity) // @filename: provider.ts // ---cut--- import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Author, Book } from "./entities" export const orm = new MikroORM({ entities: [Author, Book], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) ``` ```ts twoslash [index.ts] // @filename: entities.ts import "reflect-metadata" import { mikroSilk } from "@gqloom/mikro-orm" import { Collection } from "@mikro-orm/core" import { Entity, ManyToOne, OneToMany, PrimaryKey, Property } from "@mikro-orm/decorators/legacy" @Entity() export class AuthorEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public name!: string @OneToMany(() => BookEntity, (b: BookEntity) => b.author) public books = new Collection(this) } @Entity() export class BookEntity { @PrimaryKey({ autoincrement: true }) public id!: number @Property() public title!: string @ManyToOne(() => AuthorEntity, { ref: true }) public author!: AuthorEntity } export const Author = mikroSilk(AuthorEntity) export const Book = mikroSilk(BookEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Author, Book } from "./entities" export const orm = new MikroORM({ entities: [Author, Book], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { weave } from "@gqloom/core" import { MikroWeaver, MikroResolverFactory } from "@gqloom/mikro-orm" import { Author, Book } from "./entities" import { orm, useEm } from "./provider" const authorResolver = new MikroResolverFactory(Author, useEm).resolver() const bookResolver = new MikroResolverFactory(Book, useEm).resolver() export const schema = weave( MikroWeaver.config({ metadata: orm.getMetadata() }), authorResolver, bookResolver ) ``` ::: Now we can use them in resolvers: ### Manual Resolver You can use MikroORM entities wrapped with `mikroSilk` directly in the resolver: ```ts twoslash title="resolver.ts" // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const User = mikroSilk(UserEntity) export const Post = mikroSilk(PostEntity) // @filename: provider.ts import type { Middleware } from "@gqloom/core" import { createMemoization, useResolvingFields } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) export const useSelectedFields = () => { return Array.from(useResolvingFields()?.selectedFields ?? ["*"]) as [] } export const flusher: Middleware = async ({ next }) => { const result = await next() await useEm().flush() return result } // @filename: index.ts // ---cut--- import { field, mutation, query, resolver } from "@gqloom/core" import * as v from "valibot" import { Post, User } from "./entities" import { flusher, useEm, useSelectedFields } from "./provider" export const userResolver = resolver.of(User, { user: query(User.nullable()) .input({ id: v.number() }) .resolve(async ({ id }) => { const user = await useEm().findOne( User, { id }, { fields: useSelectedFields() } ) return user }), users: query(User.list()).resolve(() => { return useEm().findAll(User, { fields: useSelectedFields() }) }), createUser: mutation(User) .input({ data: v.object({ name: v.string(), email: v.string(), }), }) .use(flusher) .resolve(async ({ data }) => { const user = useEm().create(User, data) useEm().persist(user) return user }), posts: field(Post.list()) .derivedFrom("id") .resolve((user) => { return useEm().find( Post, { author: user.id }, { fields: useSelectedFields() } ) }), }) ``` As shown in the code above, we can use MikroORM entities wrapped with `mikroSilk` directly in the resolver. Here we use `User` as the parent type for `resolver.of`, and define two queries `user` and `users`, plus a `createUser` mutation. ::: tip Key points * **Entity Manager**: Use `useEm()` to get the request-scoped Entity Manager for database operations. * **Auto persist**: Use the `flusher` middleware to call `em.flush()` after a successful mutation. * **Performance**: Use `useSelectedFields()` so only the columns requested in the GraphQL query are selected; this function requires [enabling context](../context). ::: ### Derived Fields Add derived fields to database entities: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const Post = mikroSilk(PostEntity) // @filename: resolver.ts // ---cut--- import { field, resolver } from "@gqloom/core" import * as v from "valibot" import { type IUser, User } from "./entities" export const userResolver = resolver.of(User, { display: field(v.string()) .derivedFrom("name", "email") .resolve((user) => { return `${user.name} <${user.email}>` }), }) ``` ::: tip Note Derived fields must use `derivedFrom` to declare the columns they depend on, so that `useSelectedFields` can select them correctly. ::: ### Hiding Fields `@gqloom/mikro-orm` exposes all fields by default. To hide sensitive fields (e.g. password), use `field.hidden`: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), password: p.string(), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) // @filename: index.ts // ---cut--- import { field, resolver } from "@gqloom/core" import { User } from "./entities" export const userResolver = resolver.of(User, { password: field.hidden, }) ``` With `password: field.hidden`, that field will not appear in the generated GraphQL schema. ### Mixing Fields For fields such as `json` or `enum`, to get consistent type inference in both TypeScript and GraphQL you can use `valibot` or `zod`: ```ts twoslash import { mikroSilk } from "@gqloom/mikro-orm" import { asEnumType } from "@gqloom/valibot" import { defineEntity, type InferEntity, p } from "@mikro-orm/core" import * as v from "valibot" const Role = v.pipe( v.picklist(["admin", "user"]), asEnumType({ name: "Role", valuesConfig: { admin: { description: "Admin user" }, user: { description: "Regular user" }, }, }) ) const ContactInformation = v.object({ email: v.nullish(v.string()), phone: v.nullish(v.string()), address: v.nullish(v.string()), }) const UserEntity = defineEntity({ name: "User", properties: { id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), name: p.string(), role: p.enum(Role.options).onCreate(() => "user"), contactInformation: p .json>() .nullable(), }, }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity, { fields: { role: Role, contactInformation: v.nullish(ContactInformation), }, }) ``` ```ts twoslash import { mikroSilk } from "@gqloom/mikro-orm" import { asEnumType } from "@gqloom/zod" import { defineEntity, type InferEntity, p } from "@mikro-orm/core" import * as z from "zod" const Role = z.enum(["admin", "user"]).register(asEnumType, { valuesConfig: { admin: { description: "Admin user" }, user: { description: "Regular user" }, }, }) const ContactInformation = z.object({ email: z.string().nullish(), phone: z.string().nullish(), address: z.string().nullish(), }) const UserEntity = defineEntity({ name: "User", properties: { id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), name: p.string(), role: p.enum(Role.options).onCreate(() => "user"), contactInformation: p.json>().nullable(), }, }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity, { fields: { role: Role, contactInformation: z.nullish(ContactInformation), }, }) ``` ### Using kyselySilk Besides `mikroSilk`, `@gqloom/mikro-orm` provides `kyselySilk` — a Silk wrapper for [Kysely](https://kysely.dev/). When using Kysely with MikroORM to build raw SQL queries, you can use `kyselySilk` to convert entities to Silks that match the database table structure. Unlike `mikroSilk`, which expands relations into nested objects, `kyselySilk` focuses on low-level column mapping: * **Snake-case naming**: Converts camelCase property names to snake\_case by default. * **Foreign-key expansion**: Expands ManyToOne relations into foreign key columns (e.g., `author_name`). * **Collection omission**: Since Kysely is a table-level query builder, OneToMany and ManyToMany relations have no corresponding columns in the current table and thus are not exposed as GraphQL fields. ```ts twoslash import { defineEntity } from "@mikro-orm/core" // Define entities const AuthorEntity = defineEntity({ name: "Author", properties: (p) => ({ name: p.string().primary(), }), }) const BookEntity = defineEntity({ name: "Book", properties: (p) => ({ isbn: p.string().primary(), salesRevenue: p.float(), // camelCase property name title: p.string(), isPublished: p.boolean().default(false), author: () => p.manyToOne(AuthorEntity).ref(), // ManyToOne relation tags: () => p.manyToMany(AuthorEntity), // ManyToMany not exposed }), }) // ---cut--- import { kyselySilk } from "@gqloom/mikro-orm" import { resolver, field, weave } from "@gqloom/core" // Create Kysely Silks const Author = kyselySilk(AuthorEntity) const Book = kyselySilk(BookEntity) // Manually resolve relation field const bookResolver = resolver.of(Book, { author: field(Author, (book) => ({ name: book.author_name })), }) // Weave Schema export const schema = weave(Author, Book, bookResolver) ``` The generated GraphQL Schema automatically converts field names to snake\_case and resolves relations as foreign key IDs: ```graphql type Book { isbn: ID! sales_revenue: Float! title: String! is_published: Boolean! author_name: ID! author: Author! } type Author { name: ID! } ``` #### Kysely Naming Strategy If you want `kyselySilk` to keep the property names from the entity (no snake\_case conversion), specify `columnNamingStrategy: "property"` in the options: ```ts twoslash // @filename: entities.ts import { defineEntity } from "@mikro-orm/core" export const AuthorEntity = defineEntity({ name: "Author", properties: (p) => ({ name: p.string().primary(), }), }) export const BookEntity = defineEntity({ name: "Book", properties: (p) => ({ isbn: p.string().primary(), salesRevenue: p.float(), author: () => p.manyToOne(AuthorEntity).ref(), }), }) // @filename: index.ts // ---cut--- import { kyselySilk } from "@gqloom/mikro-orm" import type { MikroKyselyPluginOptions } from "@mikro-orm/libsql" import { AuthorEntity, BookEntity } from "./entities" const kyselyOptions = { columnNamingStrategy: "property", } as const satisfies MikroKyselyPluginOptions const Author = kyselySilk(AuthorEntity, kyselyOptions) const Book = kyselySilk(BookEntity, kyselyOptions) ``` With this configuration, `salesRevenue` remains camelCase and the foreign key uses the original property name `author` instead of `author_name`. ## Resolver Factory Besides manual resolvers, `@gqloom/mikro-orm` provides `MikroResolverFactory`. It greatly reduces boilerplate and quickly generates common queries, mutations, and relation fields from entity metadata. ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), role: p.string().$type<"admin" | "user">().default("user"), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), updatedAt: p .datetime() .onCreate(() => new Date()) .onUpdate(() => new Date()), published: p.boolean().default(false), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { MikroResolverFactory } from "@gqloom/mikro-orm" import { Post, User } from "./entities" import { useEm } from "./provider" export const userResolverFactory = new MikroResolverFactory(User, useEm) export const postResolverFactory = new MikroResolverFactory(Post, useEm) ``` The `MikroResolverFactory` constructor supports two forms: 1. `new MikroResolverFactory(Entity, getEntityManager)`: pass the entity and a function that returns an `EntityManager`. 2. `new MikroResolverFactory(Entity, options)`: pass the entity and an options object `{ getEntityManager, input? }`. ::: info Note The **`input`** option configures each field’s visibility and validation in filter / create / update. ::: ### Relation Fields The resolver factory provides `referenceField` and `collectionField` to define relation fields: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { field, query, resolver } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import * as v from "valibot" import { Post, User } from "./entities" import { useEm } from "./provider" export const userResolverFactory = new MikroResolverFactory(User, useEm) export const postResolverFactory = new MikroResolverFactory(Post, useEm) export const userResolver = resolver.of(User, { user: userResolverFactory.findOneQuery(), posts: userResolverFactory.collectionField('posts'), }) export const postResolver = resolver.of(Post, { author: postResolverFactory.referenceField('author'), }) ``` In the code above we use `userResolverFactory.collectionField('posts')` and `postResolverFactory.referenceField('author')` to define relation fields. `collectionField` is for `one-to-many` and `many-to-many` relations; `referenceField` is for `many-to-one` and `one-to-one` relations. ### Queries The resolver factory provides preset query methods that call the corresponding `EntityManager` methods: * [countQuery](https://mikro-orm.io/api/core/class/EntityRepository#count) — count * [findQuery](https://mikro-orm.io/api/core/class/EntityRepository#find) — list query * [findAndCountQuery](https://mikro-orm.io/api/core/class/EntityRepository#findAndCount) — list + total * [findByCursorQuery](https://mikro-orm.io/api/core/class/EntityRepository#findByCursor) — cursor pagination * [findOneQuery](https://mikro-orm.io/api/core/class/EntityRepository#findOne) — single query (nullable) * [findOneOrFailQuery](https://mikro-orm.io/api/core/class/EntityRepository#findOneOrFail) — single query (throws if not found) The `where` argument generates a Filter type. The `dialect` option in `MikroWeaver.config` controls whether PostgreSQL-only operators (e.g. `ilike`, `overlap`) are exposed, so you get a compatible API across databases. You can use them directly: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { query, resolver } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import * as v from "valibot" import { User } from "./entities" import { useEm } from "./provider" export const userResolverFactory = new MikroResolverFactory(User, useEm) export const userResolver = resolver.of(User, { user: userResolverFactory.findOneQuery(), posts: userResolverFactory.collectionField('posts'), }) ``` In the code above we use `userResolverFactory.findOneQuery()` to define the `user` query. The resolver factory will automatically create the input type and resolver function. ### Mutations The resolver factory provides preset mutation methods: * [createMutation](https://mikro-orm.io/api/core/class/EntityRepository#create) — create and persist * [insertMutation](https://mikro-orm.io/api/core/class/EntityRepository#insert) — native insert * [insertManyMutation](https://mikro-orm.io/api/core/class/EntityRepository#insertMany) — batch insert * [deleteMutation](https://mikro-orm.io/api/core/class/EntityRepository#nativeDelete) — delete by condition * [updateMutation](https://mikro-orm.io/api/core/class/EntityRepository#nativeUpdate) — update by condition * [upsertMutation](https://mikro-orm.io/api/core/class/EntityRepository#upsert) — upsert (update or insert) * [upsertManyMutation](https://mikro-orm.io/api/core/class/EntityRepository#upsertMany) — batch upsert ::: tip Built-in persist The factory’s mutation methods already call `em.flush()`; you usually do not need to add a flusher middleware manually. ::: You can use them directly: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export interface IPost extends InferEntity {} export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { resolver } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import { Post } from "./entities" import { useEm } from "./provider" export const postResolverFactory = new MikroResolverFactory(Post, useEm) export const postResolver = resolver.of(Post, { createPost: postResolverFactory.createMutation(), author: postResolverFactory.referenceField('author'), }) ``` In the code above we use `postResolverFactory.createMutation()` to define the `createPost` mutation. The factory will automatically create the input type and resolver function. ### Custom Input Fields Via the `input` option in the constructor, you can configure each field’s validation and visibility per operation: ```ts twoslash import { createMemoization } from "@gqloom/core/context" import { type InferEntity, defineEntity } from "@mikro-orm/core" import { EntityManager } from "@mikro-orm/libsql" const User = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), createdAt: p.datetime().onCreate(() => new Date()), email: p.string(), name: p.string(), password: p.string().nullable(), role: p.string().$type<"admin" | "user">().default("user"), }), }) export interface IUser extends InferEntity {} const useEm = createMemoization(() => ({}) as EntityManager) // ---cut--- import { field } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import * as v from "valibot" const userFactory = new MikroResolverFactory(User, { getEntityManager: useEm, input: { email: v.pipe(v.string(), v.email()), // Validate email format [!code hl] password: { filters: field.hidden, // Hide this field in query filters [!code hl] create: v.pipe(v.string(), v.minLength(6)), // Validate min length 6 on create [!code hl] update: v.pipe(v.string(), v.minLength(6)), // Validate min length 6 on update [!code hl] }, }, }) ``` ### Custom Input Object To specify the full input type (including transform) for a given query or mutation, use the `.input()` method: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), }), }) export interface IUser extends InferEntity {} export const User = mikroSilk(UserEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { User } from "./entities" export const orm = new MikroORM({ entities: [User], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { resolver } from "@gqloom/core" import { MikroResolverFactory } from "@gqloom/mikro-orm" import * as v from "valibot" import { User } from "./entities" import { useEm } from "./provider" export const userResolverFactory = new MikroResolverFactory(User, useEm) export const userResolver = resolver.of(User, { user: userResolverFactory.findOneQuery().input( v.pipe( v.object({ id: v.number() }), v.transform(({ id }) => ({ where: { id } })) ) ), }) ``` The example above transforms the input into MikroORM query parameters. ### Adding Middleware Preset queries, mutations, and fields all support the `use` method for middleware such as auth or logging: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), name: p.string() }), }) export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { resolver } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { MikroResolverFactory } from "@gqloom/mikro-orm" import { GraphQLError } from "graphql" import { Post } from "./entities" import { useEm } from "./provider" const postResolverFactory = new MikroResolverFactory(Post, useEm) const useAuthedUser = createMemoization(async () => ({ id: 1, name: "test" })) const postResolver = resolver.of(Post, { createPost: postResolverFactory.createMutation().use(async (next) => { const user = await useAuthedUser() if (user == null) throw new GraphQLError("Please login first") return next() }), }) ``` In the code above we use the `use` method to add middleware. `useAuthedUser()` is a custom function to get the current user; if not logged in it throws, otherwise it calls `next()` to continue. ### Complete Resolver You can generate a resolver that includes all preset operations directly from the factory: ```ts twoslash // @filename: entities.ts import { mikroSilk } from "@gqloom/mikro-orm" import { type InferEntity, defineEntity } from "@mikro-orm/core" const UserEntity = defineEntity({ name: "User", properties: (p) => ({ id: p.integer().primary().autoincrement(), email: p.string(), name: p.string(), posts: () => p.oneToMany(PostEntity).mappedBy("author"), }), }) export const User = mikroSilk(UserEntity) const PostEntity = defineEntity({ name: "Post", properties: (p) => ({ id: p.integer().primary().autoincrement(), title: p.string(), author: () => p.manyToOne(UserEntity).ref(), }), }) export const Post = mikroSilk(PostEntity) // @filename: provider.ts import { createMemoization } from "@gqloom/core/context" import { MikroORM } from "@mikro-orm/libsql" import { Post, User } from "./entities" export const orm = new MikroORM({ entities: [User, Post], dbName: ":memory:", }) export const useEm = createMemoization(() => orm.em.fork()) // @filename: index.ts // ---cut--- import { MikroResolverFactory } from "@gqloom/mikro-orm" import { User } from "./entities" import { useEm } from "./provider" export const userResolverFactory = new MikroResolverFactory(User, useEm) // Readonly Resolver const userQueriesResolver = userResolverFactory.queriesResolver() // Full Resolver const userResolver = userResolverFactory.resolver() ``` `MikroResolverFactory` provides two methods to generate a Resolver: * **`queriesResolver(name?)`**: Creates a resolver that only contains queries and relation fields. * **`resolver(name?)`**: Adds mutation fields (e.g. `createUser`, `updateUser`) on top of queries and relation fields. ::: info Note The optional `name` argument controls the field name prefix. For example, passing `"User"` yields `findOneUser`, `createUser`, etc. ::: ## Weaver Config and Custom Type Mapping Configure weaving behavior via `MikroWeaver.config`. Set it once in your app and pass it into `weave`: * **`presetGraphQLType(property)`**: Override the default type mapping. * **`dialect`**: Set the database dialect (e.g. `"PostgreSQL"`, `"MySQL"`, `"SQLite"`, `"MongoDB"`) to narrow Filter operators. Example: map `datetime` to `GraphQLDateTime`. ```ts twoslash import { MikroWeaver } from "@gqloom/mikro-orm" import { GraphQLDateTime } from "graphql-scalars" export const mikroWeaverConfig = MikroWeaver.config({ presetGraphQLType: (property) => { if (property.type === "datetime") { return GraphQLDateTime } }, }) ``` Pass this config when weaving the GraphQL schema: ```ts export const schema = weave(mikroWeaverConfig, userResolver, postResolver) ``` ## Default Type Mapping GQLoom maps MikroORM property types to GraphQL types by default: | MikroORM Type | GraphQL Type | | ------------- | ---------------- | | (primary) | `GraphQLID` | | string | `GraphQLString` | | number | `GraphQLFloat` | | float | `GraphQLFloat` | | double | `GraphQLFloat` | | decimal | `GraphQLFloat` | | integer | `GraphQLInt` | | smallint | `GraphQLInt` | | mediumint | `GraphQLInt` | | tinyint | `GraphQLInt` | | bigint | `GraphQLInt` | | boolean | `GraphQLBoolean` | | (other) | `GraphQLString` | --- --- url: /docs/schema/drizzle.md --- # Drizzle [Drizzle](https://orm.drizzle.team/) is a modern, type-safe TypeScript ORM designed for Node.js. It offers a concise and easy-to-use API, supports databases such as PostgreSQL, MySQL, and SQLite, and has powerful query builders, transaction processing, and database migration capabilities. At the same time, it remains lightweight and has no external dependencies, making it very suitable for database operation scenarios that require high performance and type safety. `@gqloom/drizzle` provides the integration of GQLoom and Drizzle: * Use Drizzle Table as [Silk](../silk); * Use the resolver factory to quickly create CRUD operations from Drizzle. | Drizzle | Relational Queries | `drizzle-orm` | `@gqloom/drizzle` | | --- | --- | --- | --- | | **v1** (`@rc`) | v2 | `1.0.0-rc.4+` | `0.17.0-rc.0+` (`@gqloom/drizzle@rc`) | | v0 | v1 | `0.x` | `0.16.x` | ## Installation Please refer to Drizzle's [Getting Started guide](https://orm.drizzle.team/docs/get-started) and [Upgrading to Drizzle v1](https://orm.drizzle.team/docs/upgrade-v1). After completing the Drizzle installation, install `@gqloom/drizzle@rc`: ::: code-group ```sh [npm] npm i graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [pnpm] pnpm add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [yarn] yarn add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [bun] bun add graphql @gqloom/core drizzle-orm@rc @gqloom/drizzle@rc ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:drizzle-orm@rc npm:@gqloom/drizzle@rc ``` ::: Please refer to Drizzle's [Getting Started guide](https://orm.drizzle.team/docs/get-started) to install Drizzle and the corresponding database integration. After completing the Drizzle installation, install `@gqloom/drizzle`: ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/drizzle ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/drizzle ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/drizzle ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/drizzle ``` ```sh [deno] deno add npm:graphql npm:@gqloom/core npm:@gqloom/drizzle ``` ::: ## Using Silk We can easily use Drizzle Schemas as [Silk](../silk) by simply wrapping them with `drizzleSilk`. ```ts twoslash title="schema.ts" tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) ``` ```ts twoslash title="relations.ts" tab="relations.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts // ---cut--- import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) ``` ```ts twoslash title="schema.ts" // @paths: {"@gqloom/drizzle":["node_modules/@gqloom/drizzle-rqbv1/dist/index.d.ts"],"@gqloom/drizzle/context":["node_modules/@gqloom/drizzle-rqbv1/dist/context.d.ts"],"drizzle-orm":["node_modules/drizzle-orm-rqbv1/index.d.ts"],"drizzle-orm/sqlite-core":["node_modules/drizzle-orm-rqbv1/sqlite-core/index.d.ts"],"drizzle-orm/libsql":["node_modules/drizzle-orm-rqbv1/libsql/index.d.ts"]} import { drizzleSilk } from "@gqloom/drizzle" import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), })) ``` Let's use them in the resolver. At the same time, we use the `useSelectedColumns()` function to know which columns are needed for the current GraphQL query: ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts // ---cut--- import { field, query, resolver } from "@gqloom/core" import { useSelectedColumns } from "@gqloom/drizzle/context" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { posts, users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) export const usersResolver = resolver.of(users, { user: query .output(users.$nullable()) .input({ id: v.number() }) .resolve(({ id }) => { return db .select(useSelectedColumns(users)) .from(users) .where(eq(users.id, id)) .get() }), users: query.output(users.$list()).resolve(() => { return db.select(useSelectedColumns(users)).from(users).all() }), posts: field .output(posts.$list()) .derivedFrom("id") .load(async (userList) => { const postList = await db .select() .from(posts) .where( inArray( users.id, userList.map((user) => user.id) ) ) const groups = new Map() for (const post of postList) { const key = post.authorId if (key == null) continue groups.set(key, [...(groups.get(key) ?? []), post]) } return userList.map((user) => groups.get(user.id) ?? []) }), }) ``` ```ts twoslash title="resolver.ts" // @paths: {"@gqloom/drizzle":["node_modules/@gqloom/drizzle-rqbv1/dist/index.d.ts"],"@gqloom/drizzle/context":["node_modules/@gqloom/drizzle-rqbv1/dist/context.d.ts"],"drizzle-orm":["node_modules/drizzle-orm-rqbv1/index.d.ts"],"drizzle-orm/sqlite-core":["node_modules/drizzle-orm-rqbv1/sqlite-core/index.d.ts"],"drizzle-orm/libsql":["node_modules/drizzle-orm-rqbv1/libsql/index.d.ts"]} // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), })) // @filename: resolver.ts // ---cut--- import { field, query, resolver } from "@gqloom/core" import { useSelectedColumns } from "@gqloom/drizzle/context" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import * as schema from "./schema" import { posts, users } from "./schema" const db = drizzle({ schema, connection: { url: process.env.DB_FILE_NAME! }, }) export const usersResolver = resolver.of(users, { user: query .output(users.$nullable()) .input({ id: v.number() }) .resolve(({ id }) => { return db .select(useSelectedColumns(users)) .from(users) .where(eq(users.id, id)) .get() }), users: query.output(users.$list()).resolve(() => { return db.select(useSelectedColumns(users)).from(users).all() }), posts: field .output(posts.$list()) .derivedFrom("id") .load(async (userList) => { const postList = await db .select() .from(posts) .where( inArray( users.id, userList.map((user) => user.id) ) ) const groups = new Map() for (const post of postList) { const key = post.authorId if (key == null) continue groups.set(key, [...(groups.get(key) ?? []), post]) } return userList.map((user) => groups.get(user.id) ?? []) }), }) ``` As shown in the code above, we can directly use the Drizzle Table wrapped by `drizzleSilk` in the `resolver`. Here, we use `users` as the parent type of `resolver.of`, and define two queries named `user` and `users` and a field named `posts` in the resolver. Among them: * The return type of `user` is `users.$nullable()`, indicating that `user` may be null; * The return type of `users` is `users.$list()`, indicating that `users` will return a list of `users`; * The return type of the `posts` field is `posts.$list()`. In the `posts` field, we use the `userList` parameter in the `load` method. TypeScript will help us infer its type. The `load` method is a wrapper of `DataLoader`, allowing us to quickly define a `DataLoader` method and use it to batch fetch `posts`. We also use the `useSelectedColumns()` function to determine which columns need to be selected for the current GraphQL query. This function requires [enabling context](../context).\ For runtimes where the `useSelectedColumns()` function cannot be used, we can also use the `getSelectedColumns()` function to obtain the columns that need to be selected for the current query. ### Derived Fields Adding derived Fields to a database table is quite simple. However, it's important to use the `field().derivedFrom()` method to declare the columns on which the computed property depends, so that the `useSelectedColumns` method can correctly select these columns: ```ts twoslash title="schema.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int(), }) ) // @filename: resolver.ts // ---cut--- import { field, resolver } from "@gqloom/core" import * as v from "valibot" import { posts } from "./schema" export const postsResolver = resolver.of(posts, { abstract: field(v.string()) .derivedFrom("title", "content") .resolve((post) => { return `${post.title} ${post.content?.slice(0, 60)}...` }), }) ``` ### Hiding Fields Sometimes we don't want to expose all fields of the database table to the client. Consider that we have a `users` table containing a password field, where the `password` field is an encrypted password, and we don't want to expose it to the client: ```ts twoslash title="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) ``` We can use `field.hidden` in the resolver to hide the `password` field: ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) // @filename: resolver.ts // ---cut--- import { field, resolver } from "@gqloom/core" import { users } from "./schema" export const usersResolver = resolver.of(users, { password: field.hidden, }) ``` ### Mixing Fields Sometimes we use `json`, `enum` columns in database tables, and we want to correctly infer the types in both TypeScript and GraphQL. We can use libraries like `valibot` or `zod` to define these fields: ```ts twoslash import { drizzleSilk } from "@gqloom/drizzle" import { asEnumType } from "@gqloom/valibot" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import * as v from "valibot" const Role = v.pipe( v.picklist(["admin", "user"]), asEnumType({ name: "Role", valuesConfig: { admin: { description: "Admin user" }, user: { description: "Regular user" }, }, }) ) const ContactInformation = v.object({ email: v.nullish(v.string()), phone: v.nullish(v.string()), address: v.nullish(v.string()), }) export const users = drizzleSilk( sqliteTable("users", { id: integer().primaryKey({ autoIncrement: true }), createdAt: integer({ mode: "timestamp" }).$default(() => new Date()), name: text().notNull(), role: text({ enum: Role.options }).default("user"), contactInformation: text({ mode: "json" }).$type< v.InferOutput >(), }), { fields: () => ({ role: Role, contactInformation: v.nullish(ContactInformation), }), } ) ``` ```ts twoslash import { drizzleSilk } from "@gqloom/drizzle" import { asEnumType } from "@gqloom/zod" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import * as z from "zod" const Role = z.enum(["admin", "user"]).register(asEnumType, { valuesConfig: { admin: { description: "Admin user" }, user: { description: "Regular user" }, }, }) const ContactInformation = z.object({ email: z.string().nullish(), phone: z.string().nullish(), address: z.string().nullish(), }) export const users = drizzleSilk( sqliteTable("users", { id: integer().primaryKey({ autoIncrement: true }), createdAt: integer({ mode: "timestamp" }).$default(() => new Date()), name: text().notNull(), role: text({ enum: Role.options as ["admin", "user"] }).default("user"), contactInformation: text({ mode: "json" }).$type< z.infer >(), }), { fields: () => ({ role: Role, contactInformation: z.nullish(ContactInformation), }), } ) ``` ## Resolver Factory `gqloom/drizzle` provides a resolver factory `DrizzleResolverFactory` to easily create CRUD resolvers from Drizzle, and it also supports custom parameters and adding middleware. ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts // ---cut--- import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, users) ``` ```ts twoslash // @paths: {"@gqloom/drizzle":["node_modules/@gqloom/drizzle-rqbv1/dist/index.d.ts"],"@gqloom/drizzle/context":["node_modules/@gqloom/drizzle-rqbv1/dist/context.d.ts"],"drizzle-orm":["node_modules/drizzle-orm-rqbv1/index.d.ts"],"drizzle-orm/sqlite-core":["node_modules/drizzle-orm-rqbv1/sqlite-core/index.d.ts"],"drizzle-orm/libsql":["node_modules/drizzle-orm-rqbv1/libsql/index.d.ts"]} // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id], }), })) // @filename: resolver.ts // ---cut--- import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, users) ``` ### Relationship Fields In Drizzle Table, we can easily create [relationships](https://orm.drizzle.team/docs/relations). We can use the `relationField` method of the resolver factory to create corresponding GraphQL fields for relationships. ```ts twoslash [resolver.ts] // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { field, EasyDataLoader } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { posts } from "schema" // ---cut--- import { query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, users) const usePostsLoader = createMemoization( // [!code --] () => // [!code --] new EasyDataLoader< // [!code --] { id: number }, // [!code --] (typeof posts.$inferSelect)[] // [!code --] >(async (userList) => { // [!code --] const postList = await db // [!code --] .select() // [!code --] .from(posts) // [!code --] .where( // [!code --] inArray( // [!code --] users.id, // [!code --] userList.map((user) => user.id) // [!code --] ) // [!code --] ) // [!code --] const groups = new Map() // [!code --] // [!code --] for (const post of postList) { // [!code --] const key = post.authorId // [!code --] if (key == null) continue // [!code --] groups.set(key, [...(groups.get(key) ?? []), post]) // [!code --] } // [!code --] return userList.map((user) => groups.get(user.id) ?? []) // [!code --] }) // [!code --] ) // [!code --] export const usersResolver = resolver.of(users, { user: query .output(users.$nullable()) .input({ id: v.number() }) .resolve(({ id }) => { return db.select().from(users).where(eq(users.id, id)).get() }), users: query.output(users.$list()).resolve(() => { return db.select().from(users).all() }), posts_: field.output(posts.$list()) // [!code --] .derivedFrom('id') // [!code --] .resolve((user) => { // [!code --] return usePostsLoader().load(user) // [!code --] }), // [!code --] posts: usersResolverFactory.relationField("posts"), // [!code ++] }) ``` ### Queries The Drizzle resolver factory pre-defines some commonly used queries: * `selectArrayQuery`: Find multiple records in the corresponding table according to the conditions. * `selectSingleQuery`: Find a single record in the corresponding table according to the conditions. * `countQuery`: Count the number of records in the corresponding table according to the conditions. We can use the queries from the resolver factory in the resolver: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, "users") // ---cut--- export const usersResolver = resolver.of(users, { user_: query // [!code --] .output(users.$nullable()) // [!code --] .input({ id: v.number() }) // [!code --] .resolve(({ id }) => { // [!code --] return db.select().from(users).where(eq(users.id, id)).get() // [!code --] }), // [!code --] user: usersResolverFactory.selectSingleQuery(), // [!code ++] users_: query.output(users.$list()).resolve(() => { // [!code --] return db.select().from(users).all() // [!code --] }), // [!code --] users: usersResolverFactory.selectArrayQuery(), // [!code ++] posts: usersResolverFactory.relationField("posts"), }) ``` ### Mutations The Drizzle resolver factory predefines some commonly used mutations: * `insertArrayMutation`: Insert multiple records. * `insertSingleMutation`: Insert a single record. * `updateMutation`: Update records. * `deleteMutation`: Delete records. We can use the mutations from the resolver factory in the resolver: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, "users") // ---cut--- export const usersResolver = resolver.of(users, { user: usersResolverFactory.selectSingleQuery(), users: usersResolverFactory.selectArrayQuery(), createUser: usersResolverFactory.insertSingleMutation(), // [!code ++] createUsers: usersResolverFactory.insertArrayMutation(), // [!code ++] posts: usersResolverFactory.relationField("posts"), }) ``` ### Custom Input The pre-defined queries and mutations of the resolver factory support custom input. You can define the input type through the `input` option: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, "users") // ---cut--- export const usersResolver = resolver.of(users, { user: usersResolverFactory.selectSingleQuery().input( v.pipe( // [!code hl] v.object({ id: v.number() }), // [!code hl] v.transform(({ id }) => ({ where: eq(users.id, id) })) // [!code hl] ) // [!code hl] ), users: usersResolverFactory.selectArrayQuery(), posts: usersResolverFactory.relationField("posts"), }) ``` In the above code, we use `valibot` to define the input type. `v.object({ id: v.number() })` defines the type of the input object, and `v.transform(({ id }) => ({ where: eq(users.id, id) }))` converts the input parameters into Drizzle query parameters. ### Adding Middleware The pre-defined queries, mutations, and fields of the resolver factory support adding middleware. You can define middleware through the `middlewares` option: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { query, field, resolver } from "@gqloom/core" import { createMemoization } from "@gqloom/core/context" import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import { GraphQLError } from "graphql" import * as v from "valibot" import { relations } from "./relations" import { posts, users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const postsResolverFactory = drizzleResolverFactory(db, "posts") const useAuthedUser = createMemoization( async ()=> ({ id: 0, name: "", })) // ---cut--- const postResolver = resolver.of(posts, { createPost: postsResolverFactory.insertSingleMutation().use(async (next) => { // [!code hl] const user = await useAuthedUser() // [!code hl] if (user == null) throw new GraphQLError("Please login first") // [!code hl] return next() // [!code hl] }), // [!code hl] author: postsResolverFactory.relationField("author"), authorId: field.hidden, }) ``` In the above code, we use the `middlewares` option to define middleware. `async (next) => { ... }` defines a middleware. `useAuthedUser()` is a custom function used to get the currently logged-in user. If the user is not logged in, an error is thrown; otherwise, `next()` is called to continue execution. ### Complete Resolver We can directly create a complete Resolver with the resolver factory: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( t.sqliteTable("users", { id: t.int().primaryKey({ autoIncrement: true }), name: t.text().notNull(), age: t.int(), email: t.text(), password: t.text(), }) ) export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), title: t.text().notNull(), content: t.text(), authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) // @filename: relations.ts import { defineRelations } from "drizzle-orm" import * as tables from "./schema" export const relations = defineRelations(tables, (r) => ({ users: { posts: r.many.posts({ from: r.users.id, to: r.posts.authorId, }), }, posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id, }), }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" import { relations } from "./relations" import { users } from "./schema" const db = drizzle({ relations, connection: { url: process.env.DB_FILE_NAME! }, }) const usersResolverFactory = drizzleResolverFactory(db, "users") // ---cut--- // Readonly Resolver const usersQueriesResolver = usersResolverFactory.queriesResolver() // Full Resolver const usersResolver = usersResolverFactory.resolver() ``` There are two functions for creating Resolvers: * `usersResolverFactory.queriesResolver()`: Creates a Resolver that only includes queries and relational fields. * `usersResolverFactory.resolver()`: Creates a Resolver that includes all queries, mutations, and relational fields. ## Custom Type Mapping To adapt to more Drizzle types, we can extend GQLoom to add more type mappings. First, we use `DrizzleWeaver.config` to define the configuration of type mapping. Here we import `GraphQLDateTime` and `GraphQLJSON` from [graphql-scalars](https://the-guild.dev/graphql/scalars). When encountering `date` and `json` types, we map them to the corresponding GraphQL scalars. Drizzle v1 uses `extractExtendedColumnType`; v0 uses `column.dataType`. ```ts twoslash import { extractExtendedColumnType } from "drizzle-orm" import { GraphQLDateTime, GraphQLJSON } from "graphql-scalars" import { DrizzleWeaver } from "@gqloom/drizzle" const drizzleWeaverConfig = DrizzleWeaver.config({ presetGraphQLType: (column) => { const { constraint } = extractExtendedColumnType(column) if (constraint === "date") { return GraphQLDateTime } if (constraint === "json") { return GraphQLJSON } }, }) ``` ```ts twoslash // @paths: {"@gqloom/drizzle":["node_modules/@gqloom/drizzle-rqbv1/dist/index.d.ts"],"@gqloom/drizzle/context":["node_modules/@gqloom/drizzle-rqbv1/dist/context.d.ts"],"drizzle-orm":["node_modules/drizzle-orm-rqbv1/index.d.ts"],"drizzle-orm/sqlite-core":["node_modules/drizzle-orm-rqbv1/sqlite-core/index.d.ts"],"drizzle-orm/libsql":["node_modules/drizzle-orm-rqbv1/libsql/index.d.ts"]} import { GraphQLDateTime, GraphQLJSON } from "graphql-scalars" import { DrizzleWeaver } from "@gqloom/drizzle" const drizzleWeaverConfig = DrizzleWeaver.config({ presetGraphQLType: (column) => { if (column.dataType === "date") { return GraphQLDateTime } if (column.dataType === "json") { return GraphQLJSON } }, }) ``` Pass the configuration to the `weave` function when weaving the GraphQL Schema: ```ts import { weave } from "@gqloom/core" export const schema = weave(drizzleWeaverConfig, usersResolver, postsResolver) ``` ## Default Type Mapping The following table lists the default mapping relationships between Drizzle `dataType` and GraphQL types in GQLoom: | Drizzle `dataType` | GraphQL Type | | ------------------ | ---------------- | | boolean | `GraphQLBoolean` | | number | `GraphQLFloat` | | json | `GraphQLString` | | date | `GraphQLString` | | bigint | `GraphQLString` | | string | `GraphQLString` | | buffer | `GraphQLList` | | array | `GraphQLList` | --- --- url: /docs/schema/prisma.md --- # Prisma [Prisma ORM](https://www.prisma.io/orm) offers developers a brand - new experience when working with databases, thanks to its intuitive data models, automatic migrations, type safety, and auto - completion features. `@gqloom/prisma` provides the integration of GQLoom and Prisma: * Generate [silk](../silk) from Prisma Schema. * Use the resolver factory to quickly create CRUD operations from Prisma. ## Installation Please refer to Prisma's [documentation](https://www.prisma.io/docs) to install Prisma and the corresponding database driver. ::: code-group ```sh [npm] npm i graphql @gqloom/core @gqloom/prisma ``` ```sh [pnpm] pnpm add graphql @gqloom/core @gqloom/prisma ``` ```sh [yarn] yarn add graphql @gqloom/core @gqloom/prisma ``` ```sh [bun] bun add graphql @gqloom/core @gqloom/prisma ``` ```sh [deno] deno add npm:graphql npm:prisma npm:@gqloom/core npm:@gqloom/prisma ``` ::: ## Configuration Define your Prisma Schema in the `prisma/schema.prisma` file: ```prisma title="schema.prisma" generator client { provider = "prisma-client-js" } generator gqloom { // [!code hl] provider = "prisma-gqloom" // [!code hl] } // [!code hl] datasource db { provider = "sqlite" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int } ``` ### Generator Parameters The `generator` accepts the following parameters: | Parameter | Description | Default Value | | -------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `gqloomPath` | The path to the GQLoom package. | `@gqloom/prisma` | | `clientOutput` | The path to the Prisma client. | `node_modules/@prisma/client` | | `output` | The folder path where the generated files will be located. | `node_modules/@gqloom/prisma/generated` | | `commonjsFile` | The file name of the CommonJS file. Use an empty string `""` to skip generation of the CommonJS file. | `index.cjs` | | `moduleFile` | The file name of the ES module file. Use an empty string `""` to skip generation of the ES module file. | `index.js` | | `typesFiles` | The file name(s) of the TypeScript declaration file(s). Use `[]` to skip generation of the TypeScript declaration file(s). | `["index.d.ts"]` | ### Generate Silk ```sh npx prisma generate ``` ## Using Silk After generating the silk, we can use it in the `resolver`. We use `useSelectedFields` to ensure only the fields required by the GraphQL query are selected: ```ts import { resolver, query, field, weave } from '@gqloom/core' import { asyncContextProvider } from '@gqloom/core/context' import { useSelectedFields } from "@gqloom/prisma/context" import { ValibotWeaver } from '@gqloom/valibot' import { Post, User } from '@gqloom/prisma/generated' import * as v from 'valibot' import { PrismaClient } from '@prisma/client' const db = new PrismaClient({}) const userResolver = resolver.of(User, { user: query(User.nullable(), { input: { id: v.number() }, resolve: ({ id }) => { return db.user.findUnique({ select: useSelectedFields(User), where: { id }, }) }, }), posts: field(Post.list(), async (user) => { const posts = await db.user .findUnique({ where: { id: user.id } }) .posts({ select: useSelectedFields(Post) }) return posts ?? [] }), }) const postResolver = resolver.of(Post, { author: field(User.nullable()) .derivedFrom("authorId") .resolve((post) => { if (!post.authorId) return null return db.user.findUnique({ where: { id: post.authorId } }) }), }) export const schema = weave(asyncContextProvider, ValibotWeaver, userResolver, postResolver) ``` As shown in the code above, we can directly use the types generated by Prisma within the `resolver`. Here, we've defined two resolvers: `userResolver` and `postResolver`. In `userResolver`, we use `User` as the parent type for `resolver.of` and define two fields: * The `user` query: The return type is `User.nullable()`, indicating it may return a single user or null. It accepts an `id` parameter and uses Prisma's `findUnique` method to query the database. * The `posts` field: The return type is `Post.list()`, meaning it returns a list of all articles by the user. It fetches the user's articles through Prisma's relational queries. In `postResolver`, we use `Post` as the parent type and define one field: * The `author` field: The return type is `User`, representing the author of the article. It retrieves the author information through Prisma's relational queries. All queries utilize the `useSelectedFields()` function to ensure that only the fields requested in the GraphQL query are selected. This helps optimize database query performance. This function requires [enabling context](../context). For runtimes where the `useSelectedFields()` function cannot be used, we can also use the `getSelectedFields()` function to obtain the columns that need to be selected for the current query. ### Derived Fields Adding derived fields to the model is quite simple. However, it's important to use the `field().derivedFrom()` method to declare the columns on which it depends, so that the `useSelectedFields` method can correctly select these columns: ```ts export const postResolver = resolver.of(Post, { abstract: field(v.string()) .derivedFrom("title", "content") .resolve((post) => { return `${post.title} ${post.content?.slice(0, 60)}...` }), }) ``` ### Hiding Fields `@gqloom/prisma` exposes all fields by default. If you want to hide certain fields, you can use `field.hidden`: ```ts const postResolver = resolver.of(Post, { author: field(User, async (post) => { const author = await db.post.findUnique({ where: { id: post.id } }).author() return author! }), authorId: field.hidden, // [!code hl] }) ``` In the above code, we hide the `authorId` field, which means it will not appear in the generated GraphQL Schema. ## Model Configuration You can customize output fields, input behavior, and metadata for a specific Prisma model via the `.config()` method on the generated silk. ### Output Field Configuration You can use the `fields` option to customize the GraphQL Object Type generated from the model. This lets you override field types, add descriptions, or hide specific fields. ```ts import { User } from '@gqloom/prisma/generated' import { weave, SYMBOLS } from '@gqloom/core' import { GraphQLID } from 'graphql' const userConfig = User.config({ description: "System user information", // Add description to the GraphQL type fields: { // Override field description email: { description: "User's unique email address" }, // Override field type; supports GraphQL type or silk id: { type: GraphQLID }, // Hide field so it does not appear in query results password: SYMBOLS.FIELD_HIDDEN, }, }) export const schema = weave(userConfig, userResolver, postResolver) ``` ### Input Field Behavior You can use the `input` option to control how fields behave in various input types (e.g. `CreateInput`, `UpdateInput`, `WhereInput`). You can decide per "operation" whether a field is visible or override its input type. Supported operation types: * `create`: Input for create operations (e.g. `UserCreateInput`). * `update`: Input for update operations (e.g. `UserUpdateInput`). * `filters`: Input for filter operations (e.g. `UserWhereInput`). ```ts import { User } from '@gqloom/prisma/generated' import { weave } from '@gqloom/core' import * as v from 'valibot' const userConfig = User.config({ input: { // Hide email in create email: { create: false }, // Override name in update with a required string (via silk) name: { update: v.string() }, // By default hide filter for all fields "*": { filters: false }, // Only enable filter for id id: { filters: true }, }, }) export const schema = weave(userConfig, userResolver, postResolver) ``` :::tip Priority For input types, behavior defined in the `input` option has the highest priority; it overrides both `fields` config and global presets. ::: ## Resolver Factory `@gqloom/prisma` provides the `PrismaResolverFactory` to help you create resolver factories. With the resolver factory, you can quickly define common queries, mutations, and fields. The resolver factory also pre-defines input types for common operations. Using it can greatly reduce boilerplate, which is helpful for fast iteration. ```ts import { Post, User } from '@gqloom/prisma/generated' import { PrismaResolverFactory } from '@gqloom/prisma' import { PrismaClient } from '@prisma/client' const db = new PrismaClient({}) const userResolverFactory = new PrismaResolverFactory(User, db) const postResolverFactory = new PrismaResolverFactory(Post, db) ``` In the above code, we create resolver factories for the `User` and `Post` models. `PrismaResolverFactory` accepts two arguments: the first is the model used as silk, the second is a `PrismaClient` instance. ### Relationship Fields The resolver factory provides the `relationField` method to define relationship fields: ```ts const userResolver = resolver.of(User, { user: query(User.nullable(), { input: { id: v.number() }, resolve: ({ id }) => { return db.user.findUnique({ where: { id } }) }, }), posts: field(Post.list(), async (user) => { // [!code --] const posts = await db.user.findUnique({ where: { id: user.id } }).posts() // [!code --] return posts ?? [] // [!code --] }), // [!code --] posts: userResolverFactory.relationField('posts'), // [!code ++] }) const postResolver = resolver.of(Post, { author: field(User, async (post) => { // [!code --] const author = await db.post.findUnique({ where: { id: post.id } }).author() // [!code --] return author! // [!code --] }), // [!code --] author: postResolverFactory.relationField('author'), // [!code ++] authorId: field.hidden, }) ``` In the above code, we use `userResolverFactory.relationField('posts')` and `postResolverFactory.relationField('author')` to define relationship fields. The `relationField` method accepts a string argument: the name of the relationship field. ### Queries The resolver factory pre-defines common queries: * countQuery * findFirstQuery * findManyQuery * findUniqueQuery You can use them directly: ```ts const userResolver = resolver.of(User, { user: query(User.nullable(), { // [!code --] input: { id: v.number() }, // [!code --] resolve: ({ id }) => { // [!code --] return db.user.findUnique({ where: { id } }) // [!code --] }, // [!code --] }), // [!code --] user: userResolverFactory.findUniqueQuery(), // [!code ++] posts: userResolverFactory.relationField('posts'), }) ``` In the above code, we use `userResolverFactory.findUniqueQuery()` to define the `user` query. The resolver factory creates the input type and resolver function automatically. ### Mutations The resolver factory pre-defines common mutations: * createMutation * createManyMutation * deleteMutation * deleteManyMutation * updateMutation * updateManyMutation * upsertMutation You can use them directly: ```ts const postResolver = resolver.of(Post, { createPost: postResolverFactory.createMutation(), // [!code hl] author: postResolverFactory.relationField('author'), authorId: field.hidden, }) ``` In the above code, we use `postResolverFactory.createMutation()` to define the `createPost` mutation. The factory creates the input type and resolver function automatically. ### Custom Input The resolver factory’s pre-defined queries and mutations support custom input. You can define the input type via the `input` option: ```ts import * as v from "valibot" const userResolver = resolver.of(User, { user: userResolverFactory.findUniqueQuery().input( v.pipe( // [!code hl] v.object({ id: v.number() }), // [!code hl] v.transform(({ id }) => ({ where: { id } })) // [!code hl] ) // [!code hl] ), posts: userResolverFactory.relationField("posts"), }) ``` In the above code, we use `valibot` to define the input type. `v.object({ id: v.number() })` defines the input object type, and `v.transform(({ id }) => ({ where: { id } }))` maps the input to Prisma query arguments. ### Adding Middleware The resolver factory’s pre-defined queries, mutations, and fields support middleware. You can define it via the `middlewares` option: ```ts const postResolver = resolver.of(Post, { createPost: postResolverFactory.createMutation().use(async (next) => { const user = await useAuthedUser() // [!code hl] if (user == null) throw new GraphQLError("Please login first") // [!code hl] return next() // [!code hl] }), // [!code hl] author: postResolverFactory.relationField("author"), authorId: field.hidden, }) ``` In the above code, we use the `middlewares` option to define middleware. `async (next) => { ... }` is the middleware. `useAuthedUser()` is a custom function that returns the current user; if not logged in, it throws, otherwise we call `next()` to continue. ### Complete Resolver You can create a full resolver directly from the resolver factory: ```ts // Readonly Resolver const userQueriesResolver = userResolverFactory.queriesResolver() // Full Resolver const userResolver = userResolverFactory.resolver() ``` There are two methods: * `usersResolverFactory.queriesResolver()`: Creates a resolver with only queries and relation fields. * `usersResolverFactory.resolver()`: Creates a resolver with all queries, mutations, and relation fields. ## Custom Type Mapping To adapt to more Prisma types, we can extend GQLoom to add more type mappings. First, use `PrismaWeaver.config` to define type mapping. Here we import `GraphQLDateTime` and `GraphQLJSON` from [graphql-scalars](https://the-guild.dev/graphql/scalars). When the types are `DateTime` or `Json`, we map them to the corresponding GraphQL scalars. ```ts twoslash import { GraphQLDateTime, GraphQLJSON } from 'graphql-scalars' import { PrismaWeaver } from '@gqloom/prisma' export const prismaWeaverConfig = PrismaWeaver.config({ /** * Emit @id fields as GraphQL ID type (output types only). * Default is true. Set to false to use the underlying scalar (e.g. Int or String). */ emitIdAsIDType: false, presetGraphQLType: (type) => { switch (type) { case 'DateTime': return GraphQLDateTime case 'Json': return GraphQLJSON } }, }) ``` Pass this config into the `weave` function when building the GraphQL schema: ```ts import { weave } from "@gqloom/core" export const schema = weave(prismaWeaverConfig, userResolver, postResolver) ``` ## Default Type Mapping The following table lists the default mapping between Prisma types and GraphQL types in GQLoom: | Prisma Type | GraphQL Type | | ----------- | ---------------- | | Int @id | `GraphQLID` | | String @id | `GraphQLID` | | BigInt | `GraphQLInt` | | Int | `GraphQLInt` | | Decimal | `GraphQLFloat` | | Float | `GraphQLFloat` | | Boolean | `GraphQLBoolean` | | DateTime | `GraphQLString` | | String | `GraphQLString` | --- --- url: /docs/advanced/adapters.md --- # Adapters There are a number of GraphQL HTTP adapters in the Node.js ecosystem. Since the product of the GQLoom weave is a standard GraphQL Schema object, it works seamlessly with these adapters. Here are some popular adapters: --- --- url: /docs/advanced/adapters/yoga.md --- # Yoga GraphQL Yoga is a batteries-included cross-platform [GraphQL over HTTP spec-compliant](https://github.com/enisdenjo/graphql-http/tree/master/implementations/graphql-yoga) GraphQL server powered by [Envelop](https://envelop.dev) and [GraphQL Tools](https://graphql-tools.com) that runs anywhere; focused on easy setup, performance and great developer experience. ## Installation ::: code-group ```sh [npm] npm i graphql graphql-yoga @gqloom/core ``` ```sh [pnpm] pnpm add graphql graphql-yoga @gqloom/core ``` ```sh [yarn] yarn add graphql graphql-yoga @gqloom/core ``` ```sh [bun] bun add graphql graphql-yoga @gqloom/core ``` ::: ## Usage ```ts twoslash // @filename: resolvers.ts import { resolver, query, silk, weave } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" export const helloResolver = resolver({ hello: query( silk(new GraphQLNonNull(GraphQLString)), () => "Hello, World" ), }) // @filename: index.ts // ---cut--- import { weave } from "@gqloom/core" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" import { helloResolver } from "./resolvers" const schema = weave(helloResolver) const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ## Contexts When using GQLoom together with `Yoga`, you can use `YogaInitialContext` to label the type of context: ```ts twoslash import { useContext } from "@gqloom/core/context" import { type YogaInitialContext } from "graphql-yoga" export function useAuthorization() { return useContext().request.headers.get("Authorization") } ``` You can also learn more about contexts in the [Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs/features/context). --- --- url: /docs/advanced/adapters/mercurius.md --- # Mercurius [Mercurius](https://mercurius.dev/) is a GraphQL adapter for [Fastify](https://www.fastify.io/) ## Installation ::: code-group ```sh [npm] npm i fastify mercurius graphql @gqloom/core ``` ```sh [pnpm] pnpm add fastify mercurius graphql @gqloom/core ``` ```sh [yarn] yarn add fastify mercurius graphql @gqloom/core ``` ```sh [bun] bun add fastify mercurius graphql @gqloom/core ``` ::: ## Usage ```ts twoslash // @filename: resolvers.ts import { resolver, query, silk, weave } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" export const helloResolver = resolver({ hello: query( silk(new GraphQLNonNull(GraphQLString)), () => "Hello, World" ), }) // @filename: index.ts // ---cut--- import { weave } from "@gqloom/core" import Fastify from "fastify" import mercurius from "mercurius" import { helloResolver } from "./resolvers" const schema = weave(helloResolver) const app = Fastify() app.register(mercurius, { schema }) app.listen({ port: 4000 }, () => { console.info("Mercurius server is running on http://localhost:4000") }) ``` ## Contexts When using GQLoom together with `Mercurius`, you can use `MercuriusContext` to label the type of context: ```ts twoslash import { useContext } from "@gqloom/core/context" import { type MercuriusContext } from "mercurius" export function useAuthorization() { return useContext().reply.request.headers.authorization } ``` You can also learn more about contexts in the [Mercurius documentation](https://mercurius.dev/#/docs/context). --- --- url: /docs/advanced/adapters/apollo.md --- # Apollo [Apollo Server](https://www.apollographql.com/docs/apollo-server/) is an open-source, spec-compliant GraphQL server that's compatible with any GraphQL client, including [Apollo Client](https://www.apollographql.com/docs/react). It's the best way to build a production-ready, self-documenting GraphQL API that can use data from any source. ## Installation ::: code-group ```sh [npm] npm i graphql @apollo/server @gqloom/core ``` ```sh [pnpm] pnpm add graphql @apollo/server @gqloom/core ``` ```sh [yarn] yarn add graphql @apollo/server @gqloom/core ``` ```sh [bun] bun add graphql @apollo/server @gqloom/core ``` ::: ## Usage ```ts twoslash // @filename: resolvers.ts import { resolver, query, silk, weave } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" export const helloResolver = resolver({ hello: query( silk(new GraphQLNonNull(GraphQLString)), () => "Hello, World" ), }) // @filename: index.ts // ---cut--- import { weave } from "@gqloom/core" import { ApolloServer } from "@apollo/server" import { startStandaloneServer } from "@apollo/server/standalone" import { helloResolver } from "./resolvers" const schema = weave(helloResolver) const server = new ApolloServer({ schema }) startStandaloneServer(server, { listen: { port: 4000 }, }).then(({ url }) => { console.info(`🚀 Server ready at: ${url}`) }) ``` ## Context The default context for `Apollo Server` is an empty object, you need to pass the context to the resolvers manually. See the [Apollo Server documentation](https://www.apollographql.com/docs/apollo-server/data/context) for more information. --- --- url: /docs/advanced/adapters/hono.md --- # Hono [Hono](https://hono.dev/) is a small, simple, and extremely fast web framework built based on web standards and capable of running in various JavaScript runtime environments. It has the characteristics of zero dependencies and being lightweight, and provides a concise API and first-class TypeScript support. It is suitable for building various application scenarios such as web APIs and edge applications. ## Installation ::: code-group ```sh [npm] npm i hono @hono/graphql-server graphql @gqloom/core ``` ```sh [pnpm] pnpm add hono @hono/graphql-server graphql @gqloom/core ``` ```sh [yarn] yarn add hono @hono/graphql-server graphql @gqloom/core ``` ```sh [bun] bun add hono @hono/graphql-server graphql @gqloom/core ``` ::: ## Usage ```ts import { weave } from "@gqloom/core" import { graphqlServer } from "@hono/graphql-server" import { serve } from "@hono/node-server" import { Hono } from "hono" import { helloResolver } from "./resolvers" export const app = new Hono() const schema = weave(helloResolver) app.use("/graphql", graphqlServer({ schema, graphiql: true })) serve(app, (info) => { console.info( `GraphQL server is running on http://localhost:${info.port}/graphql` ) }) ``` ## Context When using GQLoom together with Hono, you can use Hono's `Context` to annotate the type of the context: ```ts import { useContext } from "@gqloom/core" import type { Context } from "hono" export function useAuthorization() { return useContext().req.header().authorization } ``` Learn more in the [Hono documentation](https://hono.dev/docs/api/context). --- --- url: /docs/advanced/adapters/elysia.md --- # Elysia [Elysia](https://elysiajs.com/) is an ergonomic web framework for building backend servers with Bun. Designed with simplicity and type-safety in mind, Elysia has a familiar API with extensive support for TypeScript, optimized for Bun. ## Installation ::: code-group ```sh [npm] npm i elysia @elysiajs/graphql-yoga graphql @gqloom/core ``` ```sh [pnpm] pnpm add elysia @elysiajs/graphql-yoga graphql @gqloom/core ``` ```sh [yarn] yarn add elysia @elysiajs/graphql-yoga graphql @gqloom/core ``` ```sh [bun] bun add elysia @elysiajs/graphql-yoga graphql @gqloom/core ``` ::: ## Usage ```ts import { Elysia } from 'elysia' import { query, resolver, weave } from '@gqloom/core' import { yoga } from '@elysiajs/graphql-yoga' import * as z from 'zod' import { ZodWeaver } from '@gqloom/zod' import { helloResolver } from "./resolvers" const schema = weave(helloResolver) const app = new Elysia().use(yoga({ schema })).listen(8001) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ## Contexts When using GQLoom together with `@elysiajs/graphql-yoga`, you can use `YogaInitialContext` to label the type of context: ```ts import { useContext } from '@gqloom/core' import type { YogaInitialContext } from 'graphql-yoga' export function useAuthorization() { return useContext().request.headers.get('Authorization') } ``` You can also learn more about contexts in the [Elysia documentation](https://elysiajs.com/plugins/graphql-yoga.html). --- --- url: /docs/advanced/printing-schema.md --- # Printing Schema The GraphQL Schema file is the core document that defines the data structure and operations of the GraphQL API. It uses the GraphQL Schema Definition Language (SDL) to describe information such as data types, fields, queries, mutations, and subscriptions. It serves as the basis for server-side request processing and also provides an interface document for the client, helping developers understand the available data and operations. ## Generating files from Schema We can use the `printSchema` function from the `graphql` package to print out the Schema. ```ts twoslash // @filename: resolvers.ts import { query, resolver, weave } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" export const helloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) // @filename: main.ts // ---cut--- import { weave } from "@gqloom/core" import { printSchema, lexicographicSortSchema } from "graphql" import { helloResolver } from "./resolvers" import * as fs from "fs" const schema = weave(helloResolver) const schemaText = printSchema(lexicographicSortSchema(schema)) if (process.env.NODE_ENV === "development") { fs.writeFileSync("schema.graphql", schemaText) } ``` The above code generates a `schema.graphql` file that contains all the contents of the Schema. ## Using GraphQL Schema GraphQL Schema can be used for many purposes, common uses include: * Merging Schema from multiple microservices into a [supergraph](https://www.apollographql.com/docs/federation/building-supergraphs/subgraphs-overview) for unified cross-service querying on the client side. This architecture is called [federation](./federation). * Developed and type-checked on the client side using [code generation](https://the-guild.dev/graphql/codegen). * Integrate with TypeScript for client-side development for better type checking and auto-completion during development, see [gql.tada](https://gql-tada.0no.co/) for more information. --- --- url: /docs/advanced/executor.md --- # Executor Sometimes we want to invoke resolver methods directly instead of initiating a full GraphQL query. In such cases, we can use the `resolver().toExecutor()` method to create an executor. ## Basic Example ```ts twoslash import { resolver, query, field, mutation } from "@gqloom/core" import * as v from "valibot" export const Giraffe = v.object({ __typename: v.nullish(v.literal("Giraffe")), id: v.number(), name: v.string(), birthDate: v.date(), }) export interface IGiraffe extends v.InferOutput {} const giraffes = new Map([ [1, { id: 1, name: "Spotty", birthDate: new Date("2020-01-01") }], [2, { id: 2, name: "Longneck", birthDate: new Date("2020-01-02") }], ]) export const giraffeResolver = resolver.of(Giraffe, { giraffe: query(v.nullish(Giraffe)) .input({ id: v.number() }) .resolve(({ id }) => giraffes.get(id)), giraffes: query(v.array(Giraffe)).resolve(() => Array.from(giraffes.values()) ), createGiraffe: mutation(Giraffe) .input( v.object({ name: v.string(), birthDate: v.nullish(v.date(), () => new Date()), }) ) .resolve(({ name, birthDate }) => { const id = giraffes.size + 1 const giraffe = { id, name, birthDate } giraffes.set(id, giraffe) return giraffe }), }) const giraffeExecutor = giraffeResolver.toExecutor() const Aurora = giraffeExecutor.createGiraffe({ name: "Aurora" }) // @noErrors giraffeExecutor. // ^| ``` ## Context Injection Both contexts created via the `createContext` method and memoized contexts created via the `createMemoization` method can have different values injected when creating an executor. ```ts const giraffeExecutor = giraffeResolver.toExecutor( asyncContextProvider.with(useCurrentUser.provide({ id: 9, roles: ["admin"] })) ) ``` ## Unit Testing Executors are well-suited for unit testing. Here's a simple unit test example: ```ts import { giraffeResolver } from "./giraffe" import { describe, it, expect } from "vitest" describe("giraffeResolver", () => { const giraffeExecutor = giraffeResolver.toExecutor() it("should create a giraffe", async () => { const giraffe = await giraffeExecutor.createGiraffe({ name: "Aurora" }) expect(giraffe).toBeDefined() expect(giraffe.name).toBe("Aurora") }) it("should find giraffes", async () => { const giraffes = await giraffeExecutor.giraffes() expect(giraffes).toBeDefined() expect(giraffes.map((g) => g.name)).toContain("Aurora") }) }) ``` ## Non-GraphQL Entry Points In large-scale backend applications, there are often multiple entry points to invoke application logic. Besides GraphQL, common ones include message queues, gRPC, and scheduled tasks. We can use executors at these other entry points to invoke application logic.\ Here's an example of using an executor in a scheduled task: ```ts import { giraffeResolver } from "../resolvers/giraffe" import { schedule } from "node-cron" // Create an executor instance const giraffeExecutor = giraffeResolver.toExecutor() // Schedule a task to run daily at 2 AM schedule("0 2 * * *", async () => { try { // Fetch all giraffes const giraffes = await giraffeExecutor.giraffes() // Create a new entry for each giraffe for (const giraffe of giraffes) { await giraffeExecutor.createGiraffe({ name: `${giraffe.name} Jr.`, birthDate: new Date() }) } console.log("Scheduled task completed successfully") } catch (error) { console.error("Scheduled task failed:", error) } }) ``` --- --- url: /docs/advanced/subscription.md --- # Subscription In GraphQL, Subscription allows the server to push data to the client. ## Basic Usage In `GQLoom`, we use the `subscription` function to define a subscription:. ```ts twoslash import { weave, resolver, subscription } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" const countdownResolver = resolver({ countdown: subscription(v.number()) .input({ seconds: v.pipe(v.number(), v.integer()) }) .subscribe(async function* (data) { for (let i = data.seconds; i >= 0; i--) { await new Promise((resolve) => setTimeout(resolve, 1000)) yield i } }), }) const schema = weave(countdownResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` ```ts twoslash import { weave, resolver, subscription } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" import * as z from "zod" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" const countdownResolver = resolver({ countdown: subscription(z.number()) .input({ seconds: z.number().int() }) .subscribe(async function* (data) { for (let i = data.seconds; i >= 0; i--) { await new Promise((resolve) => setTimeout(resolve, 1000)) yield i } }), }) const schema = weave(ZodWeaver, countdownResolver) const yoga = createYoga({ schema }) const server = createServer(yoga) server.listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") }) ``` In the code above, we define a `countdown` subscription that accepts a `seconds` parameter. We passed in an [asynchronous generator](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) in the subscription function, which will push a number every second until the number is 0. ## Using publish/subscribe We can also use the publish/subscribe feature provided by [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#getting-started) to push messages more easily: ```ts twoslash import { resolver, query, subscription } from "@gqloom/core" import { createPubSub } from "graphql-yoga" import * as v from "valibot" const pubSub = createPubSub<{ greeting: [string] }>() const HelloResolver = resolver({ hello: query(v.string()) .input({ name: v.string() }) .resolve(({ name }) => { const hello = `Hello, ${name}` pubSub.publish("greeting", hello) return hello }), listenGreeting: subscription(v.string()) .subscribe(() => pubSub.subscribe("greeting")) .resolve((payload) => payload), }) ``` ```ts twoslash import { resolver, query, subscription } from "@gqloom/zod" import { createPubSub } from "graphql-yoga" import * as z from "zod" const pubSub = createPubSub<{ greeting: [string] }>() const HelloResolver = resolver({ hello: query(z.string()) .input({ name: z.string() }) .resolve(({ name }) => { const hello = `Hello, ${name}` pubSub.publish("greeting", hello) return hello }), listenGreeting: subscription(z.string()) .subscribe(() => pubSub.subscribe("greeting")) .resolve((payload) => payload), }) ``` In the code above, we defined a `hello` query and a `listenGreeting` subscription. When the `hello` query is called, it publishes a `greeting` event, and the `listenGreeting` subscription subscribes to this event and pushes a message when it occurs. You can learn about the detailed usage of the Publish/Subscribe feature at [GraphQL Yoga Documentation](https://the-guild.dev/graphql/yoga-server/docs/features/subscriptions#getting-started). ## Using Subscriptions in Distributed Systems The subscription feature can work easily in a monolithic application. However, in a distributed system, the subscription feature can get complicated. You may consider using the [event-driven federated subscription](https://cosmo-docs.wundergraph.com/router/event-driven-federated-subscriptions-edfs) feature from [WunderGraph Cosmo](https://cosmo-docs.wundergraph.com/) to handle subscriptions in a distributed system. --- --- url: /docs/advanced/federation.md --- # Federation [Apollo Federation](https://www.apollographql.com/docs/federation/) allows you to declaratively combine multiple GraphQL APIs into a single federated graph. A federated graph enables clients to interact with multiple APIs through a single request. GQLoom Federation provides GQLoom's support for Apollo Federation. ## Installation ::: code-group ```sh [npm] npm i graphql @gqloom/core @apollo/subgraph @gqloom/federation ``` ```sh [pnpm] pnpm add graphql @gqloom/core @apollo/subgraph @gqloom/federation ``` ```sh [yarn] yarn add graphql @gqloom/core @apollo/subgraph @gqloom/federation ``` ```sh [bun] bun add graphql @gqloom/core @apollo/subgraph @gqloom/federation ``` ::: ## GraphQL Directives Apollo Federation directives ([Directives](https://www.apollographql.com/docs/federation/federated-schemas/federated-directives/)) are used to describe how to combine multiple GraphQL APIs into a federated graph. ### Declare Directives on Objects In GQLoom, we can declare GraphQL directives in the `directives` field of the `extensions` property of objects and fields: ::: code-group ```ts twoslash [valibot] import * as v from "valibot" import { asObjectType } from "@gqloom/valibot" export const User = v.pipe( v.object({ id: v.string(), name: v.string(), }), asObjectType({ name: "User", extensions: { directives: { key: { fields: "id", resolvable: true } }, }, }) ) export interface IUser extends v.InferOutput {} ``` ```ts [zod] import * as z from "zod" import { asObjectType } from "@gqloom/zod" export const User = z .object({ id: z.string(), name: z.string(), }) .register(asObjectType, { name: "User", extensions: { directives: { key: { fields: "id", resolvable: true } }, }, }) export interface IUser extends z.infer {} ``` ```ts twoslash [graphql.js] import { silk } from "@gqloom/core" import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql" export interface IUser { id: string name: string } export const User = silk( new GraphQLObjectType({ name: "User", fields: { id: { type: new GraphQLNonNull(GraphQLString) }, name: { type: new GraphQLNonNull(GraphQLString) }, }, extensions: { directives: { key: { fields: "id", resolvable: true } }, }, }) ) ``` ::: In the above example, we declared a `@key` directive, which marks the `id` field of the `User` object as a resolvable field. We will get the following Schema: ```graphql [GraphQL Schema] type User @key(fields: "id", resolvable: true) { id: String! name: String! } ``` ### Declare Directives on Resolvers We can also use resolvers to declare directives for objects: ```ts export const userResolver = resolver .of(User, { // ... }) .directives({ key: { fields: "id", resolvable: true } }) ``` ### Adding Directives to the Schema ```ts const schema = FederatedSchemaLoom.weave( userResolver, FederatedSchemaLoom.config({ extensions: { directives: { link: [ { url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@shareable"], }, ], }, }, }) ) ``` ### Directive Formats We have two formats for declaring directives: * Using an array: ```json { directives: [ { name: "validation", args: { regex: "/abc+/" } }, { name: "required", args: {}, } ] } ``` * Using key-value pairs: ```json { directives: { validation: { regex: "/abc+/" }, required: {} } } ``` ## Resolve Reference `@gqloom/federation` provides the `resolveReference` function to help you resolve references. ```ts twoslash import { silk } from "@gqloom/core" import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql" export interface IUser { id: string name: string } export const User = silk( new GraphQLObjectType({ name: "User", fields: { id: { type: new GraphQLNonNull(GraphQLString) }, name: { type: new GraphQLNonNull(GraphQLString) }, }, extensions: { directives: { key: { fields: "id", resolvable: true } }, }, }) ) function getUserByID(id: string): IUser { return { id, name: "Jane Smith" } } // ---cut--- import { query } from "@gqloom/core" import { resolveReference, resolver } from "@gqloom/federation" export const userResolver = resolver .of(User, { user: query(User, () => ({ id: "1", name: "John" })), }) .directives({ key: { fields: "id", resolvable: true } }) .resolveReference((user) => getUserByID(user.id)) ``` ## Weaving The `FederatedSchemaWeaver.weave` function imported from `@gqloom/federation` is used to weave the Federation Schema. Compared with `@gqloom/core`, the `FederatedSchemaWeaver.weave` function in `@gqloom/federation` will output a Schema with directives. It's also worth noting that we need to use the `printSubgraphSchema` function imported from `@apollo/subgraph` to convert the Schema to text format to preserve the directives. ```ts twoslash import { silk } from "@gqloom/core" import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql" export interface IUser { id: string name: string } export const User = silk( new GraphQLObjectType({ name: "User", fields: { id: { type: new GraphQLNonNull(GraphQLString) }, name: { type: new GraphQLNonNull(GraphQLString) }, }, extensions: { directives: { key: { fields: "id", resolvable: true } }, }, }) ) function getUserByID(id: string): IUser { return { id, name: "Jane Smith" } } import { resolver, query } from "@gqloom/core" import { resolveReference } from "@gqloom/federation" export const userResolver = resolver.of( User, { user: query(User, () => ({ id: "1", name: "John" })), }, { extensions: { ...resolveReference((user) => getUserByID(user.id)), }, } ) // ---cut--- import { FederatedSchemaLoom } from "@gqloom/federation" import { printSubgraphSchema } from "@apollo/subgraph" const schema = FederatedSchemaLoom.weave(userResolver) const schemaText = printSubgraphSchema(schema) ``` --- --- url: /docs/advanced/upload.md --- # File Upload `GQLoom` supports file uploads through GraphQL's Upload scalar.\ Below are two common integration approaches: * Using the `GraphQLUpload` scalar from `graphql-upload` or `graphql-upload-minimal` * Using the `File` type provided by `graphql-yoga` ## Core Steps * Use the `silk` function to declare the `Upload` or `File` scalar. * Use the `Upload` or `File` scalar in the `input` of a `mutation`. ## Using `GraphQLUpload` The following code example applies to `graphql-upload` or `graphql-upload-minimal`. ```ts twoslash import { mutation, resolver, silk, weave } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import { GraphQLNonNull } from "graphql" import { type FileUpload, GraphQLUpload } from "graphql-upload-minimal" import { createServer } from "node:http" import { createWriteStream } from "node:fs" import { pipeline } from "node:stream/promises" import * as path from "node:path" import * as fsPromises from "node:fs/promises" import { createYoga } from "graphql-yoga" import * as v from "valibot" const Upload = silk>(new GraphQLNonNull(GraphQLUpload)) // [!code highlight] const uploadResolver = resolver({ upload: mutation(v.string()) .input({ fileName: v.nullish(v.string()), file: Upload, // [!code highlight] }) .resolve(async ({ fileName, file }) => { const { filename, createReadStream } = await file const name = fileName ?? filename const uploadsDir = path.join(import.meta.dirname, "uploads") await fsPromises.mkdir(uploadsDir, { recursive: true }) const rs = createReadStream() const ws = createWriteStream(path.join(uploadsDir, name)) await pipeline(rs, ws) return `file uploaded: ${name}` }), }) ``` Key points: * `Upload` uses `Promise`, so you need to await it before reading `createReadStream`. * You also need to add the parsing of `Upload` in the adapter, see: * [graphql-upload](https://github.com/jaydenseric/graphql-upload) * [graphql-upload-minimal](https://github.com/flash-oss/graphql-upload-minimal) * [mercurius-upload](https://github.com/mercurius-js/mercurius-upload) ## Using `File` Type The following code example applies to the `File` type from `graphql-yoga`. ```ts twoslash import { mutation, resolver, silk, weave } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import { GraphQLNonNull, GraphQLScalarType } from "graphql" import { createServer } from "node:http" import * as path from "node:path" import * as fs from "node:fs/promises" import { createYoga } from "graphql-yoga" import * as v from "valibot" const FileScalar = silk( // [!code highlight] new GraphQLNonNull( // [!code highlight] new GraphQLScalarType({ // [!code highlight] name: "File", // [!code highlight] description: "The `File` scalar type represents a file upload.", // [!code highlight] }) // [!code highlight] ) // [!code highlight] ) // [!code highlight] const uploadResolver = resolver({ upload: mutation(v.string()) .input({ fileName: v.nullish(v.string()), file: FileScalar, // [!code highlight] }) .resolve(async ({ fileName, file }) => { const name = fileName ?? file.name const uploadsDir = path.join(import.meta.dirname, "uploads") await fs.mkdir(uploadsDir, { recursive: true }) await fs.writeFile( path.join(uploadsDir, name), Buffer.from(await file.arrayBuffer()) ) return `file uploaded: ${name}` }), }) const schema = weave(ValibotWeaver, uploadResolver) const yoga = createYoga({ schema }) createServer(yoga).listen(4000) ``` Key points: * The `File` provided by Yoga directly supports `arrayBuffer()`, suitable for small to medium files or writing to disk before processing. * Similarly, wrap it as a non-null scalar using `silk`, and add validation and permission control as needed. --- --- url: /docs/migrations/from-typegraphql.md --- # Migrating from TypeGraphQL ## Why GQLoom TypeGraphQL uses classes, decorators, and `reflect-metadata`. GQLoom weaves runtime schemas (such as Zod, Valibot, or Yup) and ORM models (such as Prisma, Drizzle, or MikroORM) into GraphQL schemas. * One runtime schema or ORM model as the single source of truth instead of maintaining separate GraphQL classes, TypeScript types, and `class-validator` decorators. TypeScript types are inferred from the schema, GraphQL types are woven from it, and runtime input validation is executed by the schema itself. * Less boilerplate: you do not write `@ObjectType` classes, field decorators, and a parallel TypeScript type layer, as validation is built into the schema. * You drop `reflect-metadata`, `experimentalDecorators`, `emitDecoratorMetadata`, global metadata registries, and built-in IoC containers (such as TypeDI). After full migration, you can uninstall `type-graphql`, `reflect-metadata`, and `class-validator`, and turn off decorator compiler flags in `tsconfig.json`. * Direct reuse of Prisma, Drizzle, and MikroORM models as silks, with resolver factories generating standard CRUD operations without manual DTO classes. * `field().load()` batches relational N+1 queries without custom DataLoader classes. * Middleware and `useContext()` replace `@Authorized` decorators and constructor injection. Subscriptions and Apollo Federation work without decorators or code generation. ## Overview and mental model | | TypeGraphQL | GQLoom | | --- | --- | --- | | Types | `@ObjectType` classes | Zod / Valibot / ORM models (silks) | | Operations | `@Resolver` classes | `resolver({ ... })` objects | | Schema | `buildSchema({ resolvers })` scans global metadata | `weave(ZodWeaver, ...resolvers)` takes them explicitly | | Validation | Optional `class-validator` | The schema itself; inputs are validated by default | | Auth / DI | `@Authorized`, `container` | Middleware, `useContext()` | Migration recommendations: * Use Zod (`@gqloom/zod`) by default. If your project already uses Valibot, or the data layer is backed by Prisma, Drizzle, or MikroORM, weave those existing schemas or models directly instead of defining duplicate DTOs. * Migrate incrementally by module. Keep `type-graphql` installed during migration. Both `GraphQLSchema` instances can coexist and be combined with [`mergeSchemas`](https://the-guild.dev/graphql/tools/docs/schema-merging), or mounted on separate HTTP endpoints. * Queries and mutations remain separate definitions in GQLoom. Do not combine them into a single handler. ## Concept map The table below highlights concepts and APIs with notable differences: | TypeGraphQL | GQLoom | Notes | | --- | --- | --- | | `@ObjectType` / `@Field` | `z.object()`, names via `__typename` or `z.meta({ title })`, descriptions via `z.describe()` or `z.meta({ description })` | Put computed fields on `resolver.of`, not on the silk | | `@InputType` / `@ArgsType` | A separate input silk | Do not reuse the object silk | | `@Resolver` + `@Query` / `@Mutation` | `resolver` + `query` / `mutation` | | | `@FieldResolver` + `@Root` | `resolver.of(Type, { field })` | Parent is the first argument of `resolve` | | `@Arg` / `@Args` | `.input({ ... })` | Arguments become one object | | `@Ctx` | [`useContext()`](../context.md) | Requires `asyncContextProvider` | | `@Info` | `useResolverPayload().info` | | | `@Authorized` + `authChecker` | [Middleware](../middleware.md) | Operations use `.use()`, fields use `field().use()` | | `@UseMiddleware` | `.use()` or `weave(..., middleware)` | Koa-style onion model | | `container` / constructor injection | Context or a module-level provider | No built-in IoC container | | `class-validator` | Zod rules | See validation gotchas below | | `emitSchemaFile` | `printSchema(lexicographicSortSchema(schema))` | See [Printing Schema](../advanced/printing-schema.md) | | `registerEnumType` | `z.enum` + `asEnumType` | | | `createUnionType` | `z.union` + `asUnionType` / `resolveType` | | | `@InterfaceType` | `asObjectType({ interfaces })` | See interfaces gotcha below for polymorphic queries | | `@Directive` / `@Extensions` | `extensions` (Federation: [Federation](../advanced/federation.md)) | | | `complexity` | `extensions.complexity` | | | `DataLoader` | [`field().load()`](../dataloader.md) | | | `orphanedTypes` | Pass unused silks into `weave` | | Common field options mapping: * `description` on silk fields → `z.describe()` or `z.meta({ description })` (operations use `.description()`) * `deprecationReason` → `.deprecationReason()` * `nullable: true` → `.nullish()` (see [Gotchas](#gotchas)) * `defaultValue` → Zod `.default(...)` `asObjectType` and `asField` are the last line of defense between a Zod schema and the GraphQL schema. Declare ordinary names and descriptions with `z.describe()` or `z.meta()`. Reach for `asObjectType` and `asField` only when Zod metadata cannot express GraphQL-only configurations, such as interfaces (`asObjectType({ interfaces })`), hiding a field or overriding its GraphQL type (`asField({ type })`), complexity, or extensions. ## Migration practices ### Scaffold Install `graphql`, `@gqloom/core`, `zod`, `@gqloom/zod`, and your HTTP server adapter. New GQLoom files do not require `experimentalDecorators`. ```ts twoslash import { weave } from "@gqloom/core" import { asyncContextProvider } from "@gqloom/core/context" import { ZodWeaver } from "@gqloom/zod" import { resolver, query } from "@gqloom/core" import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()).resolve(() => "Hello"), }) export const schema = weave( ZodWeaver, asyncContextProvider, // [!code hl] helloResolver ) ``` To match TypeGraphQL's `DateTimeISO` scalar, map `z.date()` using `ZodWeaver.config` and [`GraphQLDateTimeISO`](https://the-guild.dev/graphql/scalars) (where the scalar name in SDL is `DateTimeISO`): ```ts twoslash import { ZodWeaver } from "@gqloom/zod" import { GraphQLDateTimeISO } from "graphql-scalars" import * as z from "zod" export const zodWeaverConfig = ZodWeaver.config({ presetGraphQLType: (schema) => { if (schema instanceof z.ZodDate) return GraphQLDateTimeISO }, }) ``` Pass `zodWeaverConfig` to `weave`. For HTTP server setup, see [Adapters](../advanced/adapters/). `ZodWeaver.config` with `GraphQLDateTimeISO` maps all `z.date()` instances to the SDL scalar name `DateTimeISO` (ISO string). TypeGraphQL's `graphql-scalars` example uses `Timestamp` (unix milliseconds). These are different scalars; pick the one that matches your existing SDL rather than copying the `DateTimeISO` configuration blindly. `presetGraphQLType` for `DateTimeISO` on `z.date()` still follows Zod nullability (`z.date()` maps to `DateTimeISO!`, `.nullish()` maps to nullable `DateTimeISO`). Per-field custom scalars (`NonEmptyString`, `NonNegativeInt`, `Timestamp`, etc.) go through `asField({ type })` as the last line of defense (see [Zod](../schema/zod.md)). Unlike `presetGraphQLType`, `asField({ type: GraphQLTimestamp })` replaces the GraphQL type directly and does not retain the non-null `!` modifier: `z.date().register(asField, { type: GraphQLTimestamp })` weaves a nullable `Timestamp`. To match non-null `Timestamp!`, wrap the scalar in `new GraphQLNonNull(GraphQLTimestamp)` (imported from `graphql`). The same applies to `GraphQLNonEmptyString`. ```ts import { asField } from "@gqloom/zod" import { GraphQLNonEmptyString, GraphQLTimestamp } from "graphql-scalars" import { GraphQLNonNull } from "graphql" import * as z from "zod" const Recipe = z.object({ title: z.string().register(asField, { type: new GraphQLNonNull(GraphQLNonEmptyString), }), creationDate: z.date().register(asField, { type: new GraphQLNonNull(GraphQLTimestamp), }), }) ``` ### Types and resolvers Name object types using `__typename` or `z.meta({ title })`. Define input types as separate silks named with `z.meta({ title })`. Reserve `asObjectType` as the last line of defense for GraphQL-only configurations. Queries, mutations, and computed fields are defined in `resolver` or `resolver.of`. See the [reference implementation](#reference-implementation) for a complete example. ### Auth and context ```ts twoslash import type { Middleware } from "@gqloom/core" import { GraphQLError } from "graphql" import { useContext } from "@gqloom/core/context" interface Context { user?: { roles: string[] } } export function authGuard(...roles: string[]): Middleware { return async (next) => { const user = useContext().user if (user == null) throw new GraphQLError("Not authenticated") if (roles.length > 0 && !roles.some((role) => user.roles.includes(role))) { throw new GraphQLError("Not authorized") } return next() } } ``` Attach the middleware to an individual operation with `.use(authGuard("ADMIN"))`, or pass it to `weave` as global middleware. TypeGraphQL `@Authorized()` or `@Authorized("ADMIN")` on a class field is not the same as on a query or mutation. Keep the property on the object silk so the GraphQL SDL includes the field (such as `ratings` or `ingredients`). Place the authorization guard on `field().use(authGuard())` or `field().use(authGuard("ADMIN"))` inside `resolver.of`. Fields defined only on silks do not execute middleware. Operations continue to use `.use(authGuard())` as shown above. ```ts import { field, resolver } from "@gqloom/core" import * as z from "zod" const Recipe = z.object({ ratings: z.array(z.int()) }).meta({ title: "Recipe" }) export const recipeResolver = resolver.of(Recipe, { ratings: field(z.array(z.int())) .use(authGuard("ADMIN")) .resolve((recipe) => recipe.ratings), }) ``` Services previously injected via constructor parameters should be moved to the request context or module-level providers. Do not instantiate services with `new Service()` inside `resolve` unless the service is completely stateless. ### ORMs If your project already uses Prisma, Drizzle, or MikroORM, pass the entity models directly as silks and generate standard CRUD operations using resolver factories instead of translating `@Entity` and `@ObjectType` into Zod schemas manually. See [Prisma](../schema/prisma.md#resolver-factory), [Drizzle](../schema/drizzle.md#resolver-factory), and [MikroORM](../schema/mikro-orm.md#resolver-factory). For N+1 queries on relational fields, use [`field().load()`](../dataloader.md) without creating custom DataLoader classes. ## Gotchas * Object and input silk separation: GraphQL requires output types and input types to remain distinct. TypeGraphQL uses separate `@ObjectType` and `@InputType` classes; in GQLoom, create separate silks rather than reusing an object silk as an input type. * Nullability and `.optional()`: In Zod, `.optional()` indicates that an object property may be omitted (`undefined`), but does not map to a nullable GraphQL field (`null`). To match TypeGraphQL's `nullable: true` (which produces a nullable `String`), use `.nullish()` or `.nullable()`: | TypeGraphQL | GraphQL | Zod | | --- | --- | --- | | `@Field() title: string` | `String!` | `z.string()` | | `@Field({ nullable: true }) description?: string` | `String` | `z.string().nullish()` | * Input validation and error formats: TypeGraphQL allows disabling validation via `validate: false`, whereas GQLoom has no `validate: false` and validates inputs against the schema before invoking the `resolve` function. Arguments that fail validation reject the request before execution. If your application previously depended on receiving unvalidated input, adjust the schema accordingly. The error format also changes: `class-validator` throws `ArgumentValidationError` (`extensions.validationErrors`), while GQLoom returns standard schema issues (`extensions.issues` in Zod). Update any clients that parse `extensions.validationErrors`. See [Zod](../schema/zod.md) for schema validation rules. | class-validator | Zod | | --- | --- | | `@MaxLength(n)` | `z.string().max(n)` | | `@MinLength(n)` | `z.string().min(n)` | | `@Length(min, max)` | `z.string().min(min).max(max)` | | `@Min(n)` / `@Max(n)` on int | `z.int().min(n)` / `z.int().max(n)` | When migrating nullable fields with constraints such as `@Field({ nullable: true })` and `@Length(30, 255)`, use `z.string().min(30).max(255).nullish()` with `.nullish()` on the outside of the chain, rather than `.optional()`. * `useContext()` requires `asyncContextProvider`: Calling `useContext()` requires `asyncContextProvider` to be passed into `weave`. On runtimes without `AsyncLocalStorage` support (such as certain Edge runtimes or browsers), access context directly via [`useResolverPayload().context`](../context.md#access-to-resolver-payload-directly). * `Date` types and `DateTimeISO`: By default, `z.date()` is woven as a GraphQL `String`. To match TypeGraphQL and output a `DateTimeISO` scalar, configure the scalar mapping explicitly using `ZodWeaver.config`. * Argument default values in SDL: Using `.default(...)` on an input schema applies the default value during runtime parsing. However, the generated GraphQL SDL may still show the argument as nullable rather than `Int! = 0`. * Interfaces and polymorphic queries: When implementor silks declare interfaces using `asObjectType({ interfaces: [IPerson] })`, GQLoom weaves `interface IPerson` and `type Student implements IPerson`. However, querying an interface silk directly with `query(z.array(IPerson))` causes GQLoom to weave `IPerson` as an object type. If both `query(z.array(IPerson))` and implementors referencing `interfaces: [IPerson]` exist in the same weave, GraphQL throws `Schema must contain uniquely named types but contains multiple types named "IPerson"`. To return polymorphic lists without type conflicts, use a discriminated union on the query instead: `z.discriminatedUnion("__typename", [Student, Employee])` (or `z.union` with `asUnionType` / `resolveType`), and do not call `query(z.array(IPerson))` in the same weave as those implementors. The resulting SDL produces `union Persons = Employee | Student`, and clients query fields using `__typename` and inline fragments. Note that a plain `z.string()` for an `id` field weaves as `String!`; to produce GraphQL `ID!`, use `z.string().uuid()`, `cuid()`, or `ulid()` (see [Zod](../schema/zod.md)). ```ts import { query, resolver } from "@gqloom/core" import { asObjectType } from "@gqloom/zod" import * as z from "zod" const IPerson = z.object({ __typename: z.literal("IPerson").nullish(), id: z.string(), name: z.string(), }) const Student = z .object({ __typename: z.literal("Student"), id: z.string(), name: z.string(), universityName: z.string(), }) .register(asObjectType, { interfaces: [IPerson] }) const Employee = z .object({ __typename: z.literal("Employee"), id: z.string(), name: z.string(), companyName: z.string(), }) .register(asObjectType, { interfaces: [IPerson] }) const Persons = z.discriminatedUnion("__typename", [Student, Employee]) export const personResolver = resolver({ persons: query(z.array(Persons)).resolve(() => []), }) ``` ## When to stop If you encounter the following patterns, evaluate your architecture before attempting a direct migration: * NestJS integration: Projects using NestJS with TypeGraphQL or `@nestjs/graphql` code-first rely heavily on NestJS decorators and dependency injection, requiring architectural redesign. * Request-scoped IoC: TypeGraphQL's `using-scoped-container` and per-request `container.get` patterns have no container lifecycle equivalent in GQLoom. Use request-scoped context instead. * Generic resolvers, deep inheritance, and mixins: GQLoom uses functional composition. Flatten these patterns into standalone `resolver` objects or helper factory functions. * Custom parameter decorators (`createParameterDecorator`): Replace these with `useContext()` or `.input()` definitions. TypeGraphQL's `simpleResolvers` setting can generally be ignored. For federation, subscriptions, and custom scalars, see [Federation](../advanced/federation.md), [Subscription](../advanced/subscription.md), and [custom Zod mappings](../schema/zod.md#customize-type-mappings). ## Verification The goal of migration is maintaining schema contracts and operation behavior: 1. Export the existing GraphQL SDL from TypeGraphQL using `emitSchemaFile` or `printSchema`. 2. Export the GQLoom schema using the same method: ```ts twoslash import { weave } from "@gqloom/core" import { query, resolver } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" import { lexicographicSortSchema, printSchema } from "graphql" import * as z from "zod" const helloResolver = resolver({ hello: query(z.string()).resolve(() => "Hello"), }) const schema = weave(ZodWeaver, helloResolver) export const sdl = printSchema(lexicographicSortSchema(schema)) ``` 3. Compare type names, fields, arguments, nullability, and enum values between both SDL files. 4. Replay existing queries, mutations, and subscriptions to verify response `data`. Validate authorization and input validation errors against the updated error formats (see [Gotchas](#gotchas)). After all modules are migrated and verified, remove `type-graphql`, `reflect-metadata`, and `class-validator` from your dependencies, and disable `experimentalDecorators` and `emitDecoratorMetadata` in `tsconfig.json`. ## Reference implementation The following example ports the official TypeGraphQL `simple-usage` Recipe example to GQLoom. Computed fields (`specification`, `averageRating`, `ratingsCount`) are defined on `resolver.of` rather than the base object silk. ::: code-group ```ts twoslash [GQLoom] import { field, mutation, query, resolver, weave, } from "@gqloom/core" import { ZodWeaver } from "@gqloom/zod" import { GraphQLDateTimeISO } from "graphql-scalars" import * as z from "zod" const zodWeaverConfig = ZodWeaver.config({ presetGraphQLType: (schema) => { if (schema instanceof z.ZodDate) return GraphQLDateTimeISO }, }) const Recipe = z .object({ title: z.string(), description: z .string() .nullish() .describe("The recipe description with preparation info"), ratings: z.array(z.int()), creationDate: z.date(), }) .meta({ title: "Recipe", description: "Object representing cooking recipe", }) type IRecipe = z.infer const RecipeInput = z .object({ title: z.string(), description: z.string().nullish(), }) .meta({ title: "RecipeInput" }) const items: IRecipe[] = [ { title: "Recipe 1", description: "Desc 1", ratings: [0, 3, 1], creationDate: new Date("2018-04-11"), }, ] export const recipeResolver = resolver.of(Recipe, { recipe: query(Recipe.nullish()) .input({ title: z.string() }) .resolve(({ title }) => items.find((recipe) => recipe.title === title)), recipes: query(z.array(Recipe)) .description("Get all the recipes from around the world") .resolve(() => items), addRecipe: mutation(Recipe) .input({ recipe: RecipeInput }) .resolve(({ recipe }) => { const created: IRecipe = { ...recipe, ratings: [], creationDate: new Date(), } items.push(created) return created }), specification: field(z.string().nullish()) .deprecationReason("Use 'description' field instead") .resolve((recipe) => recipe.description), averageRating: field(z.number().nullish()).resolve((recipe) => { if (recipe.ratings.length === 0) return null return recipe.ratings.reduce((a, b) => a + b, 0) / recipe.ratings.length }), ratingsCount: field(z.int()) .input({ minRate: z.int().default(0) }) .resolve((recipe, { minRate }) => { return recipe.ratings.filter((rating) => rating >= minRate).length }), }) export const schema = weave(ZodWeaver, zodWeaverConfig, recipeResolver) ``` ```ts [TypeGraphQL] @ObjectType({ description: "Object representing cooking recipe" }) class Recipe { @Field() title!: string @Field({ nullable: true, description: "The recipe description with preparation info" }) description?: string @Field((type) => [Int]) ratings!: number[] @Field() creationDate!: Date @Field((type) => String, { nullable: true, deprecationReason: "Use 'description' field instead", }) get specification(): string | undefined { return this.description } @Field((type) => Float, { nullable: true }) get averageRating(): number | null { /* ... */ } } @Resolver((of) => Recipe) class RecipeResolver { @Query((returns) => Recipe, { nullable: true }) recipe(@Arg("title") title: string) { /* ... */ } @Query((returns) => [Recipe], { description: "Get all the recipes from around the world", }) recipes() { /* ... */ } @Mutation((returns) => Recipe) addRecipe(@Arg("recipe") recipeInput: RecipeInput) { /* ... */ } @FieldResolver() ratingsCount( @Root() recipe: Recipe, @Arg("minRate", (type) => Int, { defaultValue: 0 }) minRate: number, ) { /* ... */ } } ``` ::: Compared with TypeGraphQL's official SDL, `ratingsCount.minRate` weaves as a nullable argument because `.default(0)` supplies the default at parse time rather than generating `Int! = 0` in SDL. The field set, main type nullability, and the `DateTimeISO` scalar match the original schema. Related documentation: * [Zod](../schema/zod.md): `z.describe()`, `z.meta()`, enums, unions, interfaces, and `asObjectType` / `asField` as the last line of defense * [Resolver](../resolver.md): `resolver.of`, `query`, `mutation`, and `field` * [Middleware](../middleware.md): Auth, logging, and output validation * [Context](../context.md): `useContext` and `asyncContextProvider` * [DataLoader](../dataloader.md): `field().load()` --- --- url: /docs/schema/parts/naming.info.md --- ::: info 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. :::