# gqlkit gqlkit generates GraphQL schema and resolver maps from TypeScript types and functions. ## How it works 1. Write TypeScript types in `src/gqlkit/schema/` → become GraphQL types 2. Write resolver functions using `defineQuery`, `defineMutation`, `defineField` → become GraphQL resolvers 3. Run `gqlkit gen` → outputs `typeDefs` and `resolvers` to `src/gqlkit/__generated__/` ## Design principles - **Implement first**: Write types and resolvers, generate schema when ready. No edit-regenerate-implement loops. - **Just types and functions**: Plain TypeScript with a thin API. No decorators, no complex generics. - **Type-safe**: TypeScript types become GraphQL types. Resolver signatures checked at compile time. --- # What is gqlkit? Just types and functions — write TypeScript, generate GraphQL. ## Core Concept Write types and resolvers in TypeScript → run `gqlkit gen` → get GraphQL schema AST and resolver map. ## Design Principles - **Implement first**: Write types and resolvers, generate schema when ready. No edit-regenerate-implement loops. - **Just types and functions**: Plain TypeScript with a thin API. No complex generics, no decorators. - **Type-safe**: TypeScript types become GraphQL types. Resolver signatures checked at compile time. - **Fail fast with actionable errors**: Invalid resolver references, type mismatches, and convention violations. - **GraphQL-tools compatible**: Generated outputs work seamlessly with `makeExecutableSchema`. ## How It Works 1. Write TypeScript types and resolvers in `src/gqlkit/schema/` 2. Run `gqlkit gen` 3. gqlkit scans your code, builds internal type graph, and validates resolver signatures 4. Outputs `typeDefs` (GraphQL AST) and `resolvers` map to `src/gqlkit/__generated__/` --- # Getting Started ## Installation ```sh filename="npm" npm install @gqlkit-ts/runtime @graphql-tools/schema graphql npm install -D @gqlkit-ts/cli ``` ```sh filename="pnpm" pnpm add @gqlkit-ts/runtime @graphql-tools/schema graphql pnpm add -D @gqlkit-ts/cli ``` ```sh filename="yarn" yarn add @gqlkit-ts/runtime @graphql-tools/schema graphql yarn add -D @gqlkit-ts/cli ``` > [!TIP] > > If you use AI coding agents like Claude Code or Codex, run `gqlkit docs` to set up gqlkit skills. See [Coding Agents](./coding-agents) for details. ## Project Structure gqlkit expects your types and resolvers to be in `src/gqlkit/schema/`: ``` src/ └── gqlkit/ ├── context.ts # Context type definition ├── gqlkit.ts # Resolver factories └── schema/ ├── user.ts # User type and resolvers ├── post.ts # Post type and resolvers └── query.ts # Query resolvers ``` ## Set Up Context and Resolver Factories Create `src/gqlkit/context.ts` to define your context type: ```typescript export type Context = { currentUser: { id: string; name: string; email: string | null } | null; }; ``` Create `src/gqlkit/gqlkit.ts` to export resolver factories: ```typescript import { createGqlkitApis } from "@gqlkit-ts/runtime"; import type { Context } from "./context"; export const { defineQuery, defineMutation, defineSubscription, defineField } = createGqlkitApis(); ``` ## Define Your First Type and Query Create a simple User type and query in `src/gqlkit/schema/user.ts`: ```typescript import { defineQuery } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; export type User = { id: string; name: string; email: string | null; }; // NoArgs indicates this query takes no arguments export const me = defineQuery( (_root, _args, ctx) => ctx.currentUser ); ``` ## Generate Schema Run the generator: ```sh filename="npm" npm exec gqlkit gen ``` ```sh filename="pnpm" pnpm gqlkit gen ``` ```sh filename="yarn" yarn gqlkit gen ``` This will create files in `src/gqlkit/__generated__/`: - `schema.ts` - GraphQL schema AST (DocumentNode) - `resolvers.ts` - Resolver map ## Create GraphQL Schema Use `@graphql-tools/schema` to combine the generated outputs into an executable schema: ```typescript import { makeExecutableSchema } from "@graphql-tools/schema"; import { typeDefs } from "./gqlkit/__generated__/schema"; import { resolvers } from "./gqlkit/__generated__/resolvers"; export const schema = makeExecutableSchema({ typeDefs, resolvers }); ``` ## Next Steps - [HTTP Server Integration](./integration/yoga) - Connect your schema to graphql-yoga, Apollo Server, or other HTTP servers - [Object Types](./schema/objects) - Learn more about defining types - [Queries & Mutations](./schema/queries-mutations) - Advanced resolver patterns - [Subscriptions](./schema/subscriptions) - Real-time data with subscriptions --- # Schema Definition gqlkit generates GraphQL schema from your TypeScript types. All exported types from `src/gqlkit/schema/` are automatically scanned and converted to GraphQL types. ## Type Mapping gqlkit maps TypeScript types to GraphQL types as follows: | TypeScript | GraphQL | |------------|---------| | `string` | `String!` | | `number` | `Float!` | | `boolean` | `Boolean!` | | `IDString`, `IDNumber` | `ID!` | | `Int` (branded) | `Int!` | | `Float` (branded) | `Float!` | | `T \| null` | `T` (nullable) | | `T[]` | `[T!]!` | | String literal union | Enum type | | TypeScript `enum` | Enum type | | Union of object types | Union type | | `*Input` suffix types | Input Object type | | Union with `*Input` suffix | `@oneOf` input object | | `GqlInterface` | Interface type | | `GqlScalar` | Custom scalar | | `NoArgs` | Indicates resolver takes no arguments | ## Naming Conventions gqlkit generates type names using predictable conventions: For `defineQuery`, `defineMutation`, and `defineField`, GraphQL field names are also derived from exported variable names: - Default: the full export name is used as-is. - If the export name contains `$`, gqlkit uses the substring after the last `$`. - Common usage is a single prefix (for example, `Query$users` -> `users`). - If the export name ends with `$`, gqlkit reports an error. | Context | Pattern | Example | |---------|---------|---------| | Object field (inline) | `{ParentType}{Field}` | `User.profile` → `UserProfile` | | Input field (inline) | `{ParentWithoutInput}{Field}Input` | `CreateUserInput.address` → `CreateUserAddressInput` | | Query/Mutation arg | `{Resolver}{Arg}Input` | `searchUsers(filter)` → `SearchUsersFilterInput` | | Field resolver arg | `{Parent}{Field}{Arg}Input` | `User.posts(filter)` → `UserPostsFilterInput` | | Inline enum | Same as parent context | `User.status` → `UserStatus` | | Payload type | `{Resolver}Payload` | `updateUser` → `UpdateUserPayload` | For complete details on inline enum naming, see [Inline Enums](./enums.md#inline-enums). ## Project Layout Default project layout: ``` src/ gqlkit/ schema/ # Types and resolvers co-located __generated__/ # Generated files (auto-created) ``` All TypeScript files (`.ts`, `.cts`, `.mts`) under `src/gqlkit/schema/` are scanned for types and resolvers. --- # Object Types Plain TypeScript type exports become GraphQL Object types. ## Basic Usage Export a TypeScript type or interface: ```typescript // src/gqlkit/schema/user.ts import { defineQuery, defineField } from "../gqlkit"; import type { IDString, Int, NoArgs } from "@gqlkit-ts/runtime"; /** * A user in the system */ export type User = { /** Unique identifier */ id: IDString; /** Display name */ name: string; /** Age in years */ age: Int; /** Email address (null if not verified) */ email: string | null; }; export const me = defineQuery((_root, _args, ctx) => { return findUserById(ctx.currentUserId); }); /** Get user's profile URL */ export const profileUrl = defineField((parent) => { return `https://example.com/users/${parent.id}`; }); ``` ## Nullability Use union with `null` to make fields nullable: ```typescript export type User = { email: string | null; // nullable name: string; // non-null }; ``` Generates: ```graphql type User { email: String name: String! } ``` ## Arrays Arrays are automatically converted to GraphQL lists: ```typescript export type User = { tags: string[]; // [String!]! posts: Post[] | null; // [Post!] }; ``` ## Inline Objects Object type fields can use inline object literals for nested structures. gqlkit automatically generates Object types with the naming convention `{ParentTypeName}{PascalCaseFieldName}`: ```typescript export type User = { id: string; name: string; /** User's profile information */ profile: { /** User's biography */ bio: string; age: number; }; }; ``` Generates: ```graphql type User { id: String! name: String! """User's profile information""" profile: UserProfile! } type UserProfile { """User's biography""" bio: String! age: Float! } ``` Nested inline objects generate types with concatenated names (e.g., `User.profile.address` → `UserProfileAddress`). Similarly, inline string literal unions and external TypeScript enums are automatically converted to GraphQL enum types. See [Inline Enums](./enums.md#inline-enums) for details. ## Implementing Interfaces Use `GqlObject` with the `implements` option to declare that a type implements interfaces: ```typescript import { type GqlObject, type IDString } from "@gqlkit-ts/runtime"; import type { Node, Timestamped } from "./node.js"; import type { DateTime } from "./scalars.js"; /** * A user in the system. */ export type User = GqlObject< { id: IDString; name: string; email: string | null; createdAt: DateTime; }, { implements: [Node, Timestamped] } >; ``` Generates: ```graphql """A user in the system.""" type User implements Node & Timestamped { id: ID! name: String! email: String createdAt: DateTime! } ``` You can combine `implements` with `directives`: ```typescript export type Post = GqlObject< { id: IDString; title: string; createdAt: DateTime; }, { implements: [Node, Timestamped], directives: [CacheDirective<{ maxAge: 60 }>] } >; ``` See [Interfaces](./interfaces.md) for more details on defining interface types. ## Excluding Fields Use `GqlObject` with the `ignoreFields` option to exclude specific fields from the generated GraphQL schema. This is useful for internal fields that should not be exposed in the public API: ```typescript import { type GqlObject, type IDString } from "@gqlkit-ts/runtime"; /** * A user in the system. */ export type User = GqlObject< { id: IDString; name: string; email: string | null; internalId: string; // Internal field - excluded from schema cacheKey: string; // Internal field - excluded from schema }, { ignoreFields: "internalId" | "cacheKey" } >; ``` Generates: ```graphql """A user in the system.""" type User { id: ID! name: String! email: String } ``` The excluded fields (`internalId` and `cacheKey`) are not included in the generated schema, and no resolvers are generated for them. ## Invalid Field Names Field names that are not valid GraphQL identifiers are automatically skipped with a warning. Valid GraphQL names must: - Match the pattern `/^[_A-Za-z][_0-9A-Za-z]*$/` - Not start with `__` (reserved for GraphQL introspection) ```typescript export type User = { id: string; // ✅ Valid userName: string; // ✅ Valid _internal: string; // ✅ Valid (single underscore is OK) "0invalid": string; // ⚠️ Skipped: starts with a number __reserved: string; // ⚠️ Skipped: starts with __ "field-name": string; // ⚠️ Skipped: contains hyphen }; ``` Generates (invalid fields are skipped): ```graphql type User { id: String! userName: String! _internal: String! } ``` When fields are skipped, gqlkit outputs a warning with the field name and location. --- # Input Types TypeScript types with `Input` suffix are treated as GraphQL input types. ## Basic Usage ```typescript /** Input for creating a new user */ export interface CreateUserInput { name: string; email: string | null; } ``` Generates: ```graphql """Input for creating a new user""" input CreateUserInput { name: String! email: String } ``` ## Inline Objects Input types can use inline object literals for nested structures. gqlkit automatically generates Input Object types with the naming convention `{ParentTypeNameWithoutInputSuffix}{PascalCaseFieldName}Input`: ```typescript import type { GqlField, Int } from "@gqlkit-ts/runtime"; export type CreateUserInput = { name: string; /** Profile information */ profile: { bio: string | null; /** User's age with default value */ age: GqlField; }; }; ``` Generates: ```graphql input CreateUserInput { name: String! """Profile information""" profile: CreateUserProfileInput! } input CreateUserProfileInput { """User's age with default value""" age: Int = 18 bio: String } ``` Nested inline objects generate types with concatenated names (e.g., `UserProfileInput.address` → `UserProfileAddressInput`). Similarly, inline string literal unions and external TypeScript enums are automatically converted to GraphQL enum types. See [Inline Enums](./enums.md#inline-enums) for details. ## @oneOf Input Objects Union types with `Input` suffix using inline object literals generate `@oneOf` input objects. Each union member must be an inline object literal with exactly one property. Property values can be scalar types, enum types, or references to Input Object types: ```typescript /** * Specifies how to identify a product. * Exactly one field must be provided. */ export type ProductInput = | { id: string } | { name: string } | { location: LocationInput }; ``` Generates: ```graphql """ Specifies how to identify a product. Exactly one field must be provided. """ input ProductInput @oneOf { id: String location: LocationInput name: String } ``` Each property becomes a nullable field in the generated input type. The `@oneOf` directive ensures exactly one field is provided at runtime. ### Inline @oneOf in Input Fields Union types can be used as field types within Input Objects to create nested `@oneOf` input objects. The generated type name follows the convention `{ParentTypeNameWithoutInputSuffix}{PascalCaseFieldName}Input`: ```typescript export type CreateUserInput = { name: string; /** * User identifier - either by ID or email */ identifier: { id: string } | { email: string }; }; ``` Generates: ```graphql input CreateUserInput { name: String! """User identifier - either by ID or email""" identifier: CreateUserIdentifierInput! } input CreateUserIdentifierInput @oneOf { email: String id: String } ``` ### Inline @oneOf in Resolver Arguments Inline unions in resolver arguments are also converted to `@oneOf` input objects: **Query/Mutation arguments** - naming convention: `{PascalCaseResolverName}{PascalCaseArgPath}Input` ```typescript export const findUser = defineQuery< { criteria: { identifier: { id: string } | { email: string }; }; }, User | null >(...); ``` Generates: ```graphql input FindUserCriteriaIdentifierInput @oneOf { email: String id: String } input FindUserCriteriaInput { identifier: FindUserCriteriaIdentifierInput! } type Query { findUser(criteria: FindUserCriteriaInput!): User } ``` **Field resolver arguments** - naming convention: `{ParentTypeName}{PascalCaseFieldName}{PascalCaseArgPath}Input` ```typescript export const posts = defineField< User, { filter: { status: string } | { createdAfter: string } | null; }, Post[] >(...); ``` Generates: ```graphql input UserPostsFilterInput @oneOf { createdAfter: String status: String } type User { posts(filter: UserPostsFilterInput): [Post!]! } ``` ## Default Values Specify default values for Input Object fields using `GqlField` with the `defaultValue` option. ### Basic Default Values ```typescript import { type GqlField, type Int } from "@gqlkit-ts/runtime"; export type PaginationInput = { limit: GqlField; offset: GqlField; includeArchived: GqlField; }; export type SearchInput = { query: string; caseSensitive: GqlField; maxResults: GqlField; }; export type GreetingInput = { name: GqlField; prefix: GqlField; }; ``` Generates: ```graphql input PaginationInput { limit: Int! = 10 offset: Int! = 0 includeArchived: Boolean! = false } input SearchInput { query: String! caseSensitive: Boolean! = true maxResults: Int = null } input GreetingInput { name: String! = "World" prefix: String! = "Hello" } ``` ### Complex Default Values Default values support arrays, objects, and enum values: ```typescript export type Status = "ACTIVE" | "INACTIVE" | "PENDING"; export type Priority = "LOW" | "MEDIUM" | "HIGH"; export type NestedConfig = { enabled: boolean; value: Int; }; export type FilterInput = { status: GqlField; priorities: GqlField; tags: GqlField; }; export type SettingsInput = { config: GqlField; limits: GqlField; }; ``` Generates: ```graphql input FilterInput { status: Status! = ACTIVE priorities: [Priority!]! = [MEDIUM, HIGH] tags: [String!]! = ["default"] } input SettingsInput { config: NestedConfig! = {enabled: true, value: 100} limits: [Int!]! = [10, 20, 30] } ``` ### Default Values with Directives You can combine `defaultValue` with `directives`: ```typescript import { type GqlField, type GqlDirective, type Int } from "@gqlkit-ts/runtime"; export type LengthDirective = GqlDirective<"length", TArgs, "INPUT_FIELD_DEFINITION" | "ARGUMENT_DEFINITION">; export type RangeDirective = GqlDirective<"range", TArgs, "INPUT_FIELD_DEFINITION" | "ARGUMENT_DEFINITION">; export type CreateUserInput = { name: GqlField]; }>; age: GqlField]; }>; email: GqlField]; }>; }; ``` Generates: ```graphql input CreateUserInput { name: String! = "Anonymous" @length(min: 1, max: 100) age: Int! = 18 @range(min: 0, max: 150) email: String = null @length(min: 5, max: 255) } ``` ### Supported Default Value Types | Value type | Example | GraphQL output | |------------|---------|----------------| | String | `"hello"` | `"hello"` | | Number (Int) | `10` | `10` | | Number (Float) | `3.14` | `3.14` | | Boolean | `true`, `false` | `true`, `false` | | Null | `null` | `null` | | Array | `[1, 2, 3]` | `[1, 2, 3]` | | Object | `{ key: "value" }` | `{key: "value"}` | | Enum | `"ACTIVE"` (when field type is enum) | `ACTIVE` | ### Literal Types Required Default values must be specified as TypeScript literal types. Non-literal types will cause a warning: ```typescript // ✅ OK: Literal types export type GoodInput = { limit: GqlField; name: GqlField; }; // ❌ Error: Non-literal types export type BadInput = { limit: GqlField; // Warning: must be literal name: GqlField; // Warning: must be literal }; ``` ## Excluding Fields Use `GqlObject` with the `ignoreFields` option to exclude specific fields from the generated Input Object. This is useful for internal fields that should not be accepted from clients: ```typescript import { type GqlObject } from "@gqlkit-ts/runtime"; /** * Input for creating a user. */ export type CreateUserInput = GqlObject< { name: string; email: string; internalData: string; // Internal field - excluded from schema trackingId: string; // Internal field - excluded from schema }, { ignoreFields: "internalData" | "trackingId" } >; ``` Generates: ```graphql """Input for creating a user.""" input CreateUserInput { name: String! email: String! } ``` ## Invalid Field Names Input field names follow the same validation rules as object type fields. See [Invalid Field Names](./objects.md#invalid-field-names) for details. --- # Queries & Mutations Define Query and Mutation fields using the `@gqlkit-ts/runtime` API. > **Prerequisites**: This guide assumes you have completed the [basic setup](../getting-started.md#set-up-context-and-resolver-factories). ## Query Resolvers Use `defineQuery` to define Query fields. GraphQL field names are derived from the exported variable name: - Default: the full export name is used as-is. - If the export name contains `$`, gqlkit uses the substring after the last `$`. - If the export name ends with `$`, gqlkit reports an error. ```typescript import { defineQuery } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import type { User } from "./user"; // Query.me export const me = defineQuery( (_root, _args, ctx) => { return ctx.db.findUser(ctx.userId); } ); // Query.user(id: String!) export const user = defineQuery<{ id: string }, User | null>( (_root, args, ctx) => { return ctx.db.findUser(args.id); } ); // Query.users export const users = defineQuery( (_root, _args, ctx) => { return ctx.db.findAllUsers(); } ); ``` Generates: ```graphql type Query { me: User user(id: String!): User users: [User!]! } ``` Example with `$` delimiter: ```typescript // GraphQL field name: users export const Query$users = defineQuery(() => []); ``` ## Mutation Resolvers Use `defineMutation` to define Mutation fields. The same export-name rule applies: ```typescript import { defineMutation } from "../gqlkit"; import type { User, CreateUserInput } from "./user"; // Mutation.createUser(input: CreateUserInput!) export const createUser = defineMutation<{ input: CreateUserInput }, User>( (_root, args, ctx) => { return ctx.db.createUser(args.input); } ); // Mutation.deleteUser(id: String!) export const deleteUser = defineMutation<{ id: string }, boolean>( (_root, args, ctx) => { return ctx.db.deleteUser(args.id); } ); // GraphQL field name: createUser export const Mutation$createUser = defineMutation< { input: CreateUserInput }, User >((_root, args, ctx) => { return ctx.db.createUser(args.input); }); ``` Generates: ```graphql type Mutation { createUser(input: CreateUserInput!): User! deleteUser(id: String!): Boolean! } ``` ## Resolver Function Signature Query and Mutation resolvers receive four arguments: ```typescript (root, args, ctx, info) => ReturnType ``` | Argument | Description | |----------|-------------| | `root` | The root value (usually undefined) | | `args` | The arguments passed to the field | | `ctx` | The context object (typed via `createGqlkitApis()`) | | `info` | GraphQL resolve info | ## NoArgs Type Use the `NoArgs` type when a field has no arguments: ```typescript import { type NoArgs } from "@gqlkit-ts/runtime"; export const me = defineQuery( (_root, _args, ctx) => ctx.currentUser ); ``` ## Arguments with Input Types Use Input types for complex arguments: ```typescript export type SearchUsersInput = { query: string; limit: number | null; }; export const searchUsers = defineQuery<{ input: SearchUsersInput }, User[]>( (_root, args) => { return findUsers(args.input.query, args.input.limit ?? 10); } ); ``` ## Inline Object Arguments Arguments can use inline object literals. gqlkit automatically generates Input Object types: ```typescript export const searchUsers = defineQuery< { /** Search filter */ filter: { namePattern: string | null; status: UserStatus | null; }; }, User[] >((_root, args) => []); ``` Generates: ```graphql type Query { searchUsers( """Search filter""" filter: SearchUsersFilterInput! ): [User!]! } input SearchUsersFilterInput { namePattern: String status: UserStatus } ``` Inline string literal unions and external TypeScript enums in arguments are also automatically converted to GraphQL enum types. See [Inline Enums](./enums.md#inline-enums) for details. See [Field Resolvers](./fields.md) for more details on inline object arguments. ## Inline Payload Types Return types can use inline object literals. gqlkit automatically generates GraphQL Object types with the naming convention `{PascalCaseResolverName}Payload`: ```typescript export const updateUser = defineMutation< { input: UpdateUserInput }, { user: User; updatedAt: string } >((_root, args, ctx) => ({ user: ctx.db.updateUser(args.input), updatedAt: new Date().toISOString(), })); ``` Generates: ```graphql type Mutation { updateUser(input: UpdateUserInput!): UpdateUserPayload! } type UpdateUserPayload { user: User! updatedAt: String! } ``` ### Inline Union Payloads Union types with inline object literals generate GraphQL Union types. Each union member must have a `__typename` property with a string literal type: ```typescript export const updateUser = defineMutation< { input: UpdateUserInput }, | { __typename: "UpdateUserSuccess"; user: User } | { __typename: "UpdateUserError"; message: string } >((_root, args, ctx) => { const result = ctx.db.updateUser(args.input); if (result.ok) { return { __typename: "UpdateUserSuccess", user: result.user }; } return { __typename: "UpdateUserError", message: result.error }; }); ``` Generates: ```graphql type Mutation { updateUser(input: UpdateUserInput!): UpdateUserPayload! } union UpdateUserPayload = UpdateUserError | UpdateUserSuccess type UpdateUserSuccess { user: User! } type UpdateUserError { message: String! } ``` The `__resolveType` function is automatically generated based on the `__typename` property. See [Abstract Type Resolution](./abstract-resolvers.md) for more details. ### Inline Enum Payloads String literal unions in return types generate GraphQL Enum types: ```typescript export const getStatus = defineQuery( (_root, _args, ctx) => ctx.db.getStatus() ); ``` Generates: ```graphql type Query { getStatus: GetStatusPayload! } enum GetStatusPayload { ACTIVE INACTIVE PENDING } ``` ### Nested Inline Types Inline types can be nested within payload objects: ```typescript export const createOrder = defineMutation< { input: CreateOrderInput }, { order: { id: string; status: "pending" | "confirmed"; items: { productId: string; quantity: number }[]; }; } >(/* ... */); ``` Generates: ```graphql type CreateOrderPayload { order: CreateOrderPayloadOrder! } type CreateOrderPayloadOrder { id: String! status: CreateOrderPayloadOrderStatus! items: [CreateOrderPayloadOrderItems!]! } enum CreateOrderPayloadOrderStatus { PENDING CONFIRMED } type CreateOrderPayloadOrderItems { productId: String! quantity: Float! } ``` ## Attaching Directives Add a third type parameter to attach directives to Query/Mutation fields: ```typescript import { defineQuery } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import { type AuthDirective } from "./directives.js"; import type { User } from "./user.js"; export const me = defineQuery< NoArgs, User, [AuthDirective<{ role: ["USER"] }>] >((_root, _args, ctx) => ctx.currentUser); ``` Generates: ```graphql type Query { me: User! @auth(role: [USER]) } ``` See [Directives](./directives.md) for more details on defining and using custom directives. --- # Subscriptions Define Subscription fields using the `@gqlkit-ts/runtime` API. > **Prerequisites**: This guide assumes you have completed the [basic setup](../getting-started.md#set-up-context-and-resolver-factories). ## Setup Export `defineSubscription` from your `gqlkit.ts`: ```typescript import { createGqlkitApis } from "@gqlkit-ts/runtime"; import type { Context } from "./context"; export const { defineQuery, defineMutation, defineSubscription } = createGqlkitApis(); ``` ## Subscription Resolvers Use `defineSubscription` to define Subscription fields. The resolver is typically an async generator function: ```typescript import { defineSubscription } from "../gqlkit"; import type { Message } from "./message"; // Subscription.messageAdded(channelId: String!) export const messageAdded = defineSubscription< { channelId: string }, Message >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("MESSAGE_ADDED", args.channelId); }); ``` Generates: ```graphql type Subscription { messageAdded(channelId: String!): Message! } ``` The same export name conventions apply as with [Queries & Mutations](./queries-mutations.md): ```typescript // GraphQL field name: messageAdded export const Subscription$messageAdded = defineSubscription< { channelId: string }, Message >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("MESSAGE_ADDED", args.channelId); }); ``` ## NoArgs Subscriptions For subscriptions without arguments, use `NoArgs` as the first type parameter — same as with `defineQuery` and `defineMutation`. See [Queries & Mutations](./queries-mutations.md#noargs-queries) for details. ## Resolver Function Signature Subscription resolvers receive the same four arguments as Query/Mutation resolvers, but return an `AsyncIterable` instead of a direct value: ```typescript (root, args, ctx, info) => AsyncIterable | Promise> ``` | Argument | Description | |----------|-------------| | `root` | The root value (always undefined) | | `args` | The arguments passed to the field | | `ctx` | The context object (typed via `createGqlkitApis()`) | | `info` | GraphQL resolve info | ## Inline Object Arguments Subscription arguments support the same inline object types as queries and mutations: ```typescript export const orderUpdated = defineSubscription< { filter: { orderId: string | null; status: string | null; }; }, Order >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("ORDER_UPDATED", args.filter); }); ``` Generates: ```graphql type Subscription { orderUpdated(filter: OrderUpdatedFilterInput!): Order! } input OrderUpdatedFilterInput { orderId: String status: String } ``` ## Attaching Directives Add a third type parameter to attach directives: ```typescript import { defineSubscription } from "../gqlkit"; import type { AuthDirective } from "./directives"; import type { Message } from "./message"; export const messageAdded = defineSubscription< { channelId: string }, Message, [AuthDirective<{ role: ["USER"] }>] >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("MESSAGE_ADDED", args.channelId); }); ``` Generates: ```graphql type Subscription { messageAdded(channelId: String!): Message! @auth(role: [USER]) } ``` See [Directives](./directives.md) for more details on defining and using custom directives. ## Documentation TSDoc comments on subscription exports are extracted as GraphQL descriptions: ```typescript /** Subscribe to new messages in a channel. */ export const messageAdded = defineSubscription< { channelId: string }, Message >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("MESSAGE_ADDED", args.channelId); }); /** * @deprecated Use messageAdded instead. */ export const onMessage = defineSubscription< { channelId: string }, Message >(async function* (_root, args, ctx) { yield* ctx.pubsub.subscribe("MESSAGE_ADDED", args.channelId); }); ``` Generates: ```graphql type Subscription { """Subscribe to new messages in a channel.""" messageAdded(channelId: String!): Message! onMessage(channelId: String!): Message! @deprecated(reason: "Use messageAdded instead.") } ``` See [Documentation](./documentation.md) for more details. --- # Field Resolvers Add computed fields to object types using `defineField`. Define them alongside the type. GraphQL field names are derived from the exported variable name: - Default: the full export name is used as-is. - If the export name contains `$`, gqlkit uses the substring after the last `$`. - If the export name ends with `$`, gqlkit reports an error. ## Basic Usage ```typescript // src/gqlkit/schema/user.ts import { defineField } from "../gqlkit"; import type { IDString, NoArgs } from "@gqlkit-ts/runtime"; import type { Post } from "./post.js"; export type User = { id: IDString; name: string; }; /** Get posts authored by this user */ export const posts = defineField( (parent) => findPostsByAuthor(parent.id) ); /** Get user's post count */ export const postCount = defineField( (parent) => countPostsByAuthor(parent.id) ); ``` Generates: ```graphql type User { id: ID! name: String! """Get posts authored by this user""" posts: [Post!]! """Get user's post count""" postCount: Float! } ``` Example with `$` delimiter: ```typescript // GraphQL field name: posts export const User$posts = defineField( (parent) => findPostsByAuthor(parent.id) ); ``` ## Resolver Function Signature Field resolvers receive four arguments: ```typescript (parent, args, ctx, info) => ReturnType ``` | Argument | Description | |----------|-------------| | `parent` | The parent object (typed via the first type parameter) | | `args` | The arguments passed to the field | | `ctx` | The context object (typed via `gqlkit.ts`) | | `info` | GraphQL resolve info | ## Type Parameters `defineField`: | Parameter | Description | |-----------|-------------| | `TParent` | The parent object type this field is defined on | | `TArgs` | The arguments type (use `NoArgs` if no arguments) | | `TResult` | The return type of the field | ## Fields with Arguments ```typescript export const posts = defineField< User, { limit: number; offset: number }, Post[] >((parent, args) => { return findPostsByAuthor(parent.id, args.limit, args.offset); }); ``` Generates: ```graphql type User { posts(limit: Float!, offset: Float!): [Post!]! } ``` ## Inline Object Arguments Field resolver arguments can use inline object literals. gqlkit automatically generates Input Object types with the naming convention `{ParentTypeName}{PascalCaseFieldName}{PascalCaseArgName}Input`: ```typescript import { defineField } from "../gqlkit"; export const posts = defineField< User, { /** Filter options */ filter: { titlePattern: string | null; status: PostStatus | null; } | null; }, Post[] >((parent, args) => []); ``` Generates: ```graphql type User { posts( """Filter options""" filter: UserPostsFilterInput ): [Post!]! } input UserPostsFilterInput { titlePattern: String status: PostStatus } ``` Inline string literal unions and external TypeScript enums in arguments are also automatically converted to GraphQL enum types. See [Inline Enums](./enums.md#inline-enums) for details. ## Inline Payload Types Field resolver return types can use inline object literals. gqlkit automatically generates GraphQL Object types with the naming convention `{ParentTypeName}{PascalCaseFieldName}Payload`: ```typescript export const statistics = defineField< User, NoArgs, { postCount: number; followerCount: number; isActive: boolean } >((parent, _args, ctx) => ctx.db.getUserStatistics(parent.id)); ``` Generates: ```graphql type User { statistics: UserStatisticsPayload! } type UserStatisticsPayload { postCount: Float! followerCount: Float! isActive: Boolean! } ``` ### Inline Union Payloads Union types with inline object literals generate GraphQL Union types. Each union member must have a `__typename` property: ```typescript export const verification = defineField< User, NoArgs, | { __typename: "Verified"; verifiedAt: string } | { __typename: "Unverified"; reason: string } >((parent) => parent.verification); ``` Generates: ```graphql type User { verification: UserVerificationPayload! } union UserVerificationPayload = Unverified | Verified type Verified { verifiedAt: String! } type Unverified { reason: String! } ``` See [Queries & Mutations](./queries-mutations.md#inline-payload-types) for more details on inline payload types. ## Default Values in Arguments Default values in Input Objects are applied to resolver arguments: ```typescript import { defineQuery } from "../gqlkit"; import type { GqlField, Int } from "@gqlkit-ts/runtime"; export type PaginationInput = { limit: GqlField; offset: GqlField; }; export type User = { id: string; name: string; }; export const users = defineQuery(() => []); ``` Generates: ```graphql type Query { users(limit: Int! = 10, offset: Int! = 0): [User!]! } ``` ## Interface Field Resolvers Add computed fields to interface types using `defineField`: ```typescript import { defineField } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import type { Node } from "./node.js"; /** Get the typename of a Node */ export const __typename = defineField( (parent) => parent.constructor.name ); ``` ## Attaching Directives Add a fourth type parameter to attach directives to field resolvers: ```typescript import { defineField } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import { type AuthDirective } from "./directives.js"; export const email = defineField< User, NoArgs, string, [AuthDirective<{ role: ["ADMIN"] }>] >((parent) => parent.email); ``` See [Directives](./directives.md) for more details on defining and using custom directives. --- # Scalar Types gqlkit provides built-in scalar types and supports custom scalar definitions. ## Built-in Scalar Types gqlkit provides branded types to explicitly specify GraphQL scalar mappings: | TypeScript type | GraphQL type | |-----------------|--------------| | `IDString` | `ID!` | | `IDNumber` | `ID!` | | `Int` | `Int!` | | `Float` | `Float!` | | `string` | `String!` | | `number` | `Float!` | | `boolean` | `Boolean!` | ### Usage ```typescript import { type IDString, type Int, type Float } from "@gqlkit-ts/runtime"; export type User = { id: IDString; // ID! age: Int; // Int! score: Float; // Float! name: string; // String! balance: number; // Float! active: boolean; // Boolean! }; ``` ## Custom Scalars with GqlScalar Define custom scalar types using the `GqlScalar` utility type: ```typescript // src/gqlkit/schema/scalars.ts import { type GqlScalar } from "@gqlkit-ts/runtime"; /** * ISO 8601 format date-time string */ export type DateTime = GqlScalar<"DateTime", Date>; ``` Generates: ```graphql """ISO 8601 format date-time string""" scalar DateTime ``` ### GqlScalar Type Parameters `GqlScalar`: | Parameter | Description | |-----------|-------------| | `Name` | The GraphQL scalar name | | `Base` | The TypeScript type that backs this scalar | | `Only` | Optional: `"input"` or `"output"` to restrict usage | ### Input-only and Output-only Scalars Restrict scalar usage to input or output positions: ```typescript import { type GqlScalar } from "@gqlkit-ts/runtime"; // Input-only scalar (for input positions only) export type DateTimeInput = GqlScalar<"DateTime", Date, "input">; // Output-only scalar (for output positions only) export type DateTimeOutput = GqlScalar<"DateTime", Date | string, "output">; ``` This is useful when input and output representations differ. ## Config-based Custom Scalars Alternatively, map TypeScript types to GraphQL custom scalars via config: ```typescript // gqlkit.config.ts import { defineConfig } from "@gqlkit-ts/cli"; export default defineConfig({ scalars: [ { name: "DateTime", tsType: { from: "./src/gqlkit/schema/scalars", name: "DateTime" }, description: "ISO 8601 datetime string", }, { name: "UUID", tsType: { name: "string" }, // Global type only: "input", // Input-only scalar }, ], }); ``` ### Config Options | Option | Description | |--------|-------------| | `name` | The GraphQL scalar name | | `tsType.name` | The TypeScript type name | | `tsType.from` | Optional: import path for the type | | `description` | Optional: GraphQL description | | `only` | Optional: `"input"` or `"output"` | ## Using Custom Scalars Once defined, use custom scalars in your types: ```typescript import type { DateTime } from "./scalars.js"; export type User = { id: IDString; name: string; createdAt: DateTime; updatedAt: DateTime | null; }; export type CreateUserInput = { name: string; birthDate: DateTime; }; ``` ## Registering Scalar Resolvers Custom scalars require resolver implementations to handle serialization and parsing at runtime. Pass scalar resolvers to `createResolvers`: ```typescript import { createResolvers } from "./gqlkit/__generated__/resolvers.js"; const resolvers = createResolvers({ scalars: { DateTime: myDateTimeScalar, }, }); ``` ### Using graphql-scalars (Recommended) The easiest way to implement custom scalars is to use [graphql-scalars](https://the-guild.dev/graphql/scalars), which provides ready-to-use implementations for common scalar types: ```typescript import { GraphQLDateTime } from "graphql-scalars"; const resolvers = createResolvers({ scalars: { DateTime: GraphQLDateTime, }, }); ``` This approach requires no manual serialize/parse implementation. ### Custom Implementation For custom behavior, create your own `GraphQLScalarType`: ```typescript import { GraphQLScalarType, Kind } from "graphql"; const DateTimeScalar = new GraphQLScalarType({ name: "DateTime", description: "ISO 8601 date-time string", serialize(value) { return value instanceof Date ? value.toISOString() : value; }, parseValue(value) { return new Date(value as string); }, parseLiteral(ast) { if (ast.kind === Kind.STRING) { return new Date(ast.value); } return null; }, }); const resolvers = createResolvers({ scalars: { DateTime: DateTimeScalar, }, }); ``` --- # Enum Types gqlkit converts TypeScript string literal unions and enums to GraphQL enum types. ## String Literal Unions String literal unions are automatically converted to GraphQL enum types: ```typescript /** * User account status */ export type UserStatus = "ACTIVE" | "INACTIVE" | "PENDING"; ``` Generates: ```graphql """User account status""" enum UserStatus { ACTIVE INACTIVE PENDING } ``` ## TypeScript Enums TypeScript string enums are also supported: ```typescript export enum UserStatus { Active = "ACTIVE", Inactive = "INACTIVE", Pending = "PENDING", } ``` Generates: ```graphql enum UserStatus { ACTIVE INACTIVE PENDING } ``` ## Deprecating Enum Values Use the `@deprecated` JSDoc tag to mark enum values as deprecated: ```typescript export enum UserStatus { Active = "ACTIVE", /** * @deprecated Use `Inactive` instead */ Pending = "PENDING", Inactive = "INACTIVE", } ``` For string literal unions, use a separate type with JSDoc: ```typescript /** * User account status */ export type UserStatus = | "ACTIVE" | /** @deprecated Use INACTIVE instead */ "PENDING" | "INACTIVE"; ``` Generates: ```graphql enum UserStatus { ACTIVE PENDING @deprecated(reason: "Use INACTIVE instead") INACTIVE } ``` ## Using Enums in Types ```typescript export type User = { id: string; name: string; status: UserStatus; }; export type UpdateUserInput = { status: UserStatus | null; }; ``` Generates: ```graphql type User { id: String! name: String! status: UserStatus! } input UpdateUserInput { status: UserStatus } ``` ## Inline Enums When you define a string literal union or reference a TypeScript enum **inline** (without exporting it from the schema directory), gqlkit automatically generates a GraphQL enum type. This follows the same pattern as [inline objects](./objects.md#inline-objects). ### Inline String Literal Unions String literal unions used directly in field or argument types generate enum types automatically: ```typescript export type User = { id: string; name: string; /** Current account status */ status: "active" | "inactive" | "pendingReview"; }; ``` Generates: ```graphql type User { id: String! name: String! """Current account status""" status: UserStatus! } enum UserStatus { ACTIVE INACTIVE PENDING_REVIEW } ``` The generated enum type name follows the convention `{ParentTypeName}{PascalCaseFieldName}`. ### External TypeScript Enums TypeScript enums defined outside the schema directory are also automatically converted: ```typescript // src/types/order.ts (outside schema directory) /** * Order status in the system */ export enum OrderStatus { /** Order is pending payment */ Pending = "pending", /** Order is being processed */ Processing = "processing", /** Order has been shipped */ Shipped = "shipped", } // src/gqlkit/schema/order.ts import { OrderStatus } from "../../types/order.js"; export type Order = { id: string; status: OrderStatus; }; ``` Generates: ```graphql type Order { id: String! status: OrderStatus! } """Order status in the system""" enum OrderStatus { """Order is pending payment""" PENDING """Order is being processed""" PROCESSING """Order has been shipped""" SHIPPED } ``` TSDoc comments on the enum and its values are preserved as GraphQL descriptions. The `@deprecated` tag is also supported. When the same external TypeScript enum is referenced in multiple places, gqlkit generates a single GraphQL enum type and reuses it across all references. ### Inline Enum Naming Convention The naming convention for auto-generated enum types matches [inline objects](./objects.md#inline-objects): | Context | Naming Pattern | Example | |---------|----------------|---------| | Object field | `{ParentTypeName}{PascalCaseFieldName}` | `User.status` → `UserStatus` | | Input field | `{ParentTypeNameWithoutInputSuffix}{PascalCaseFieldName}Input` | `CreateUserInput.role` → `CreateUserRoleInput` | | Query/Mutation argument | `{PascalCaseFieldName}{PascalCaseArgName}Input` | `searchUsers(status: ...)` → `SearchUsersStatusInput` | | Field resolver argument | `{ParentTypeName}{PascalCaseFieldName}{PascalCaseArgName}Input` | `User.posts(filter: ...)` → `UserPostsFilterInput` | ### Nullable Inline Enums Nullable inline enums are supported: ```typescript export type User = { id: string; status: "active" | "inactive" | null; }; ``` Generates: ```graphql type User { id: String! status: UserStatus } enum UserStatus { ACTIVE INACTIVE } ``` ### Arrays of Inline Enums Inline enums in array types are also supported: ```typescript export type User = { id: string; roles: ("admin" | "editor" | "viewer")[]; }; ``` Generates: ```graphql type User { id: String! roles: [UserRoles!]! } enum UserRoles { ADMIN EDITOR VIEWER } ``` ### When Enums Are NOT Auto-Generated If you export a type from the schema directory, it is treated as an explicit type declaration and not auto-generated: ```typescript // Exported from schema - used as-is, not auto-generated export type UserStatus = "active" | "inactive" | "pending"; export type User = { id: string; status: UserStatus; // References the exported type }; ``` ### Inline Enum Payloads String literal unions in resolver return types generate GraphQL Enum types with the naming convention `{ResolverName}Payload`: ```typescript export const getStatus = defineQuery( (_root, _args, ctx) => ctx.db.getStatus() ); ``` Generates: ```graphql type Query { getStatus: GetStatusPayload! } enum GetStatusPayload { ACTIVE INACTIVE PENDING } ``` For field resolvers, the naming convention is `{ParentTypeName}{PascalCaseFieldName}Payload`: ```typescript export const status = defineField( (parent) => parent.currentStatus ); ``` Generates: ```graphql type User { status: UserStatusPayload! } enum UserStatusPayload { ONLINE OFFLINE AWAY } ``` See [Queries & Mutations](./queries-mutations.md#inline-payload-types) for more details on inline payload types. ## Automatic Case Conversion gqlkit automatically converts enum values to `SCREAMING_SNAKE_CASE` format, which is the GraphQL convention: ```typescript export type UserStatus = "active" | "inProgress" | "pending_review" | "on-hold"; ``` Generates: ```graphql enum UserStatus { ACTIVE IN_PROGRESS PENDING_REVIEW ON_HOLD } ``` When conversion changes the original value, gqlkit generates resolver mappings to translate between GraphQL and TypeScript values: ```typescript // Generated resolvers.ts export function createResolvers() { return { UserStatus: { ACTIVE: "active", IN_PROGRESS: "inProgress", PENDING_REVIEW: "pending_review", ON_HOLD: "on-hold", }, }; } ``` If values are already in `SCREAMING_SNAKE_CASE`, no resolver mapping is generated. ### Automatic Prefix Stripping When all enum values share a common prefix that matches the enum name, gqlkit automatically strips the prefix to produce cleaner GraphQL values: ```typescript export type UserStatus = "USER_STATUS_ACTIVE" | "USER_STATUS_INACTIVE" | "USER_STATUS_PENDING"; ``` Generates: ```graphql enum UserStatus { ACTIVE INACTIVE PENDING } ``` This works with various naming conventions: | TypeScript Enum | Values | Generated GraphQL Values | |-----------------|--------|--------------------------| | `UserStatus` | `USER_STATUS_ACTIVE`, `USER_STATUS_INACTIVE` | `ACTIVE`, `INACTIVE` | | `orderStatus` | `ORDER_STATUS_PENDING`, `ORDER_STATUS_SHIPPED` | `PENDING`, `SHIPPED` | | `Status` | `STATUS_ACTIVE`, `STATUS_INACTIVE` | `ACTIVE`, `INACTIVE` | Prefix stripping is **not applied** when: - Not all values share the common prefix - Stripping would result in an empty value (e.g., `USER_STATUS_` for `UserStatus`) When prefix stripping is applied, gqlkit generates resolver mappings to preserve the original TypeScript values: ```typescript // Generated resolvers.ts export function createResolvers() { return { UserStatus: { ACTIVE: "USER_STATUS_ACTIVE", INACTIVE: "USER_STATUS_INACTIVE", PENDING: "USER_STATUS_PENDING", }, }; } ``` ### Duplicate Value Detection If multiple TypeScript values convert to the same GraphQL enum value, gqlkit reports a `DUPLICATE_ENUM_VALUE_AFTER_CONVERSION` error: ```typescript // Error: 'activeUser' and 'active_user' both convert to ACTIVE_USER export type Status = "activeUser" | "active_user" | "pending"; ``` ## Invalid Enum Values Enum values that are not valid GraphQL identifiers are automatically skipped with a warning. gqlkit converts enum values to `SCREAMING_SNAKE_CASE`, and the converted name must: - Match the pattern `/^[_A-Za-z][_0-9A-Za-z]*$/` - Not start with `__` (reserved for GraphQL introspection) ### String Literal Unions ```typescript export type Status = | "active" // ✅ Converts to ACTIVE | "inProgress" // ✅ Converts to IN_PROGRESS | "0pending" // ⚠️ Skipped: converts to 0PENDING (starts with number) | "__internal"; // ⚠️ Skipped: converts to __INTERNAL (starts with __) ``` Generates: ```graphql enum Status { ACTIVE IN_PROGRESS } ``` ### TypeScript Enums ```typescript export enum Priority { HIGH = "HIGH", // ✅ Valid MEDIUM = "MEDIUM", // ✅ Valid LOW = "LOW", // ✅ Valid "0INVALID" = "0INVALID", // ⚠️ Skipped: starts with number __RESERVED = "__RESERVED", // ⚠️ Skipped: starts with __ } ``` Generates: ```graphql enum Priority { HIGH MEDIUM LOW } ``` When enum values are skipped, gqlkit outputs a warning with the original name, converted name, and location. --- # Union Types TypeScript union types of object types are converted to GraphQL union types. ## Basic Usage ```typescript import type { Post } from "./Post"; import type { Comment } from "./Comment"; /** * Content that can be searched */ export type SearchResult = Post | Comment; ``` Generates: ```graphql """Content that can be searched""" union SearchResult = Comment | Post ``` ## Using Unions in Resolvers ```typescript import { defineQuery } from "../gqlkit"; import type { SearchResult } from "./types"; export const search = defineQuery<{ query: string }, SearchResult[]>( (_root, args, ctx) => { return ctx.db.search(args.query); } ); ``` Generates: ```graphql type Query { search(query: String!): [SearchResult!]! } ``` ## Inline Unions When a field type is an inline union of object types, gqlkit automatically generates a GraphQL Union type. The generated type name follows the convention `{ParentTypeName}{PascalCaseFieldName}`: ```typescript import type { User } from "./User"; import type { Post } from "./Post"; export type SearchResult = { id: string; /** * The matched item - either a User or a Post */ item: User | Post; }; ``` Generates: ```graphql type SearchResult { id: String! """The matched item - either a User or a Post""" item: SearchResultItem! } union SearchResultItem = Post | User ``` ### Nullable Inline Unions Inline unions can be nullable: ```typescript export type Container = { id: string; result: User | Post | null; }; ``` Generates: ```graphql type Container { id: String! result: ContainerResult } union ContainerResult = Post | User ``` ### Known Type References When union members are exported types in the schema directory (`knownTypeNames`), they are preserved as references. Unknown inline object types are automatically generated: ```typescript import type { User } from "./User"; // known type export type Activity = { actor: User | { id: string; type: string }; // User is referenced, anonymous object is generated }; ``` Generates: ```graphql type Activity { actor: ActivityActor! } union ActivityActor = ActivityActorAnonymous | User type ActivityActorAnonymous { id: String! type: String! } ``` ### Validation Inline unions in output context must contain only object types. The following are not allowed: - Primitive types (string, number, boolean) - Enum types - Scalar types ```typescript // These will produce errors: export type Invalid = { value: string | number; // Error: primitives not allowed status: User | "active"; // Error: cannot mix object and enum }; ``` ## Union vs Enum - Use **Union** when each member is a distinct object type with different fields - Use **Enum** when you need a set of string constants ```typescript // Union: Different object types export type SearchResult = Post | Comment | User; // Enum: String constants export type Status = "ACTIVE" | "INACTIVE" | "PENDING"; ``` ## Union vs Interface - Use **Union** when types share no common fields - Use **Interface** when types share common fields that should be queryable ```typescript // Union: No common structure required export type SearchResult = Post | Comment; // Interface: Common fields enforced export type Node = GqlInterface<{ id: IDString; }>; ``` See [Interfaces](./interfaces.md) for more details on interface types. ## Inline Union Payloads When a resolver return type is an inline union with object literals, gqlkit generates a GraphQL Union type. Each union member must have a `__typename` property with a string literal type: ```typescript export const updateUser = defineMutation< { input: UpdateUserInput }, | { __typename: "UpdateUserSuccess"; user: User } | { __typename: "UpdateUserError"; message: string } >(/* ... */); ``` Generates: ```graphql type Mutation { updateUser(input: UpdateUserInput!): UpdateUserPayload! } union UpdateUserPayload = UpdateUserError | UpdateUserSuccess type UpdateUserSuccess { user: User! } type UpdateUserError { message: String! } ``` ### `__typename` Requirement For inline union payloads, the `__typename` property is required and must be a string literal type: ```typescript // ✅ OK: __typename with string literal type type Result = | { __typename: "Success"; data: string } | { __typename: "Error"; message: string }; // ❌ Error: __typename missing type Invalid = | { data: string } | { message: string }; // ❌ Error: __typename is not a string literal type AlsoInvalid = | { __typename: string; data: string } | { __typename: string; message: string }; ``` ### Automatic `__resolveType` Generation For inline union payloads, gqlkit automatically generates a `__resolveType` function that returns the `__typename` property value. You don't need to define it manually. If you need custom type resolution logic, use [defineResolveType](./abstract-resolvers.md#defineresolveType). ### Mixed Union Payloads You can mix inline object literals with named types. Only inline objects require `__typename`: ```typescript import type { User } from "./user"; export const findEntity = defineQuery< { id: string }, | User // named type - __typename determined by type | { __typename: "Guest"; sessionId: string } // inline type - __typename required >(/* ... */); ``` Generates: ```graphql union FindEntityPayload = Guest | User type Guest { sessionId: String! } ``` See [Queries & Mutations](./queries-mutations.md#inline-payload-types) for more details on inline payload types. ## Runtime Type Resolution When GraphQL executes a query that returns a union type, it needs to determine the concrete type at runtime. ### Automatic Resolution If your union member types have `__typename` or `$typeName` fields with string literal values, gqlkit automatically generates the `resolveType` function: ```typescript export interface User { __typename: "User"; id: string; name: string; } export interface Post { __typename: "Post"; id: string; title: string; } export type SearchResult = User | Post; // resolveType is automatically generated - no manual definition needed ``` ### Custom Discriminator Fields When union member types use a field other than `__typename` or `$typeName` for discrimination (e.g., external library types like AI SDK's `UIMessagePart`), you can configure custom discriminator fields via `gqlkit.config.ts`. gqlkit will automatically generate `__resolveType` based on the specified fields. ```ts // gqlkit.config.ts import { defineConfig } from "@gqlkit-ts/cli"; export default defineConfig({ discriminatorFields: { ContentPart: "type", }, }); ``` ```typescript // src/gqlkit/schema/types.ts export interface TextPart { type: "text"; text: string; } export interface ImagePart { type: "image"; url: string; alt: string; } export type ContentPart = TextPart | ImagePart; ``` Generates a `switch`-based `__resolveType`: ```typescript ContentPart: { __resolveType: (obj) => { switch (obj.type) { case "text": return "TextPart"; case "image": return "ImagePart"; default: return undefined; } }, }, ``` Unlike `__typename` or `$typeName`, custom discriminator fields remain as regular GraphQL fields in the generated schema: ```graphql union ContentPart = ImagePart | TextPart type TextPart { type: String! text: String! } type ImagePart { type: String! url: String! alt: String! } ``` #### Multiple Discriminator Fields When a single field is not enough to uniquely identify each member, you can specify multiple discriminator fields as an array. The first field must exist on all members with a string literal type. Secondary fields do not need to exist on every member. ```ts // gqlkit.config.ts import { defineConfig } from "@gqlkit-ts/cli"; export default defineConfig({ discriminatorFields: { Content: ["type", "mediaType"], }, }); ``` ```typescript export type Content = | { type: "text"; mediaType: "plain"; body: string } | { type: "text"; mediaType: "html"; html: string } | { type: "image"; url: string; alt: string }; ``` Generates nested `switch` statements and type names derived from the discriminator values: ```graphql union Content = ContentImage | ContentTextHtml | ContentTextPlain type ContentTextPlain { type: String! mediaType: String! body: String! } type ContentTextHtml { type: String! mediaType: String! html: String! } type ContentImage { type: String! url: String! alt: String! } ``` #### Validation Rules - The **first** discriminator field must exist on **all** union members and have a string literal type - Secondary fields do not need to exist on every member, but the combination of values must be unique across all members - If a `defineResolveType` is manually defined for the same union, the manual definition takes priority - If `discriminatorFields` is configured for a union that also has `$typeName` or `__typename`, the `discriminatorFields` configuration takes priority See [Configuration](../configuration.md#custom-discriminator-fields) for the full config reference. ### Manual Resolution For types without `__typename` or `$typeName`, use `defineResolveType`: ```typescript import { defineResolveType } from "../gqlkit"; export const searchResultResolveType = defineResolveType( (value) => { if ("name" in value) return "User"; return "Post"; } ); ``` See [Abstract Type Resolution](./abstract-resolvers.md) for complete documentation. --- # Interface Types Define GraphQL interface types using the `GqlInterface` utility type. ## Basic Usage ```typescript // src/gqlkit/schema/node.ts import { type GqlInterface, type IDString } from "@gqlkit-ts/runtime"; import type { DateTime } from "./scalars.js"; /** * Node interface - represents any entity with a unique identifier. */ export type Node = GqlInterface<{ /** Global unique identifier */ id: IDString; }>; /** * Timestamped interface - entities that track creation time. */ export type Timestamped = GqlInterface<{ createdAt: DateTime; }>; ``` Generates: ```graphql """Node interface - represents any entity with a unique identifier.""" interface Node { """Global unique identifier""" id: ID! } """Timestamped interface - entities that track creation time.""" interface Timestamped { createdAt: DateTime! } ``` ## GqlInterface Type Parameters `GqlInterface`: | Parameter | Description | |-----------|-------------| | `T` | The interface fields definition | | `Meta` | Optional: `{ implements: [...] }` for interface inheritance | ## Interface Inheritance Interfaces can extend other interfaces: ```typescript /** * Entity interface - combines Node and Timestamped. */ export type Entity = GqlInterface< { id: IDString; createdAt: DateTime; }, { implements: [Node, Timestamped] } >; ``` Generates: ```graphql """Entity interface - combines Node and Timestamped.""" interface Entity implements Node & Timestamped { id: ID! createdAt: DateTime! } ``` ## Implementing Interfaces Use `GqlObject` with the `implements` option to declare that a type implements interfaces: ```typescript import { type GqlObject, type IDString } from "@gqlkit-ts/runtime"; import type { Node, Timestamped } from "./node.js"; import type { DateTime } from "./scalars.js"; /** * A user in the system. */ export type User = GqlObject< { id: IDString; name: string; email: string | null; createdAt: DateTime; }, { implements: [Node, Timestamped] } >; ``` Generates: ```graphql """A user in the system.""" type User implements Node & Timestamped { id: ID! name: String! email: String createdAt: DateTime! } ``` ## Combining with Directives You can combine `implements` with `directives`: ```typescript import { type GqlObject, type IDString } from "@gqlkit-ts/runtime"; import type { Node, Timestamped } from "./node.js"; import type { DateTime } from "./scalars.js"; import type { CacheDirective } from "./directives.js"; export type Post = GqlObject< { id: IDString; title: string; createdAt: DateTime; }, { implements: [Node, Timestamped], directives: [CacheDirective<{ maxAge: 60 }>] } >; ``` Generates: ```graphql type Post @cache(maxAge: 60) implements Node & Timestamped { id: ID! title: String! createdAt: DateTime! } ``` ## Interface Field Resolvers Add computed fields to interface types using `defineField`: ```typescript import { defineField } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import type { Node } from "./node.js"; /** Get the typename of a Node */ export const __typename = defineField( (parent) => parent.constructor.name ); ``` See [Object Types](./objects.md) for more details on implementing interfaces. ## Runtime Type Resolution When GraphQL executes a query that returns an interface type, it needs to determine the concrete type at runtime. ### Automatic Resolution If all implementing types have `__typename` or `$typeName` fields with string literal values, gqlkit automatically generates the `resolveType` function: ```typescript export interface User { __typename: "User"; id: IDString; name: string; } export interface Post { __typename: "Post"; id: IDString; title: string; } // Both User and Post implement Node // resolveType is automatically generated - no manual definition needed ``` ### Manual Resolution For types without `__typename` or `$typeName`, use `defineResolveType` on the interface or `defineIsTypeOf` on each implementing type: ```typescript import { defineResolveType } from "../gqlkit"; export const nodeResolveType = defineResolveType((value) => { if ("name" in value) return "User"; if ("title" in value) return "Post"; throw new Error("Unknown Node type"); }); ``` See [Abstract Type Resolution](./abstract-resolvers.md) for complete documentation. --- # Abstract Type Resolution GraphQL abstract types (unions and interfaces) require runtime type resolution to determine the concrete type of returned values. gqlkit provides `defineResolveType` and `defineIsTypeOf` to handle this. ## Overview When a GraphQL query returns an abstract type, the server needs to determine which concrete type to use. There are two approaches: | Approach | Defined On | Returns | Use Case | |----------|------------|---------|----------| | `resolveType` | Abstract type (union/interface) | Type name string | Single resolver decides the type | | `isTypeOf` | Object type | Boolean | Each type checks if value matches | ## Automatic resolveType Generation When Union or Interface member types have `__typename` or `$typeName` fields with string literal values, gqlkit automatically generates the `resolveType` function. No manual definition is needed. ### Basic Example ```typescript export interface User { __typename: "User"; id: string; name: string; } export interface Post { __typename: "Post"; id: string; title: string; } export type SearchResult = User | Post; // resolveType is automatically generated: (obj) => obj.__typename ``` ### Using `$typeName` If you prefer not to use `__typename` (e.g., to avoid conflicts with GraphQL introspection), you can use `$typeName` instead: ```typescript export interface User { $typeName: "User"; id: string; name: string; } export interface Post { $typeName: "Post"; id: string; title: string; } export type SearchResult = User | Post; // resolveType is automatically generated: (obj) => obj.$typeName ``` ### Priority Rules When both `__typename` and `$typeName` are present, `__typename` takes priority: ```typescript export interface User { __typename: "User"; // This value is used $typeName: "UserType"; id: string; } ``` ### Mixed Patterns When some members use `__typename` and others use `$typeName`, gqlkit generates a fallback pattern: ```typescript export interface User { __typename: "User"; id: string; } export interface Post { $typeName: "Post"; id: string; } export type SearchResult = User | Post; // resolveType: (obj) => obj.__typename ?? obj.$typeName ``` ### Requirements For automatic generation, the typename field must be: - Named `__typename` or `$typeName` - A **non-optional** field - A **non-nullable** type - A **string literal** type (not `string`) ```typescript // ✅ OK: Valid typename fields interface Valid { __typename: "TypeA"; // string literal, required } // ❌ Error: These will not trigger auto-generation interface Invalid1 { __typename?: "TypeA"; // optional field } interface Invalid2 { __typename: "TypeA" | null; // nullable type } interface Invalid3 { __typename: string; // not a string literal } ``` ### When to Use Manual defineResolveType Use `defineResolveType` manually when: - Your types don't have `__typename` or `$typeName` fields - You need custom resolution logic (e.g., checking other properties) - You want to override the automatic generation ## Using resolveType Define a `resolveType` resolver on a union or interface type to determine the concrete type. ### Union Example ```typescript import { defineResolveType } from "../gqlkit"; export interface User { id: string; name: string; } export interface Post { id: string; title: string; } export type SearchResult = User | Post; export const searchResultResolveType = defineResolveType( (value) => { if ("name" in value) { return "User"; } return "Post"; } ); ``` ### Interface Example ```typescript import { defineResolveType } from "../gqlkit"; import { type GqlInterface, type IDString } from "@gqlkit-ts/runtime"; export type Node = GqlInterface<{ id: IDString; }>; export const nodeResolveType = defineResolveType((value) => { if ("name" in value) { return "User"; } if ("title" in value) { return "Post"; } throw new Error("Unknown Node type"); }); ``` ### Resolver Function Signature ```typescript (value: TAbstract, context: TContext, info: GraphQLResolveInfo) => string | Promise ``` | Argument | Description | |----------|-------------| | `value` | The resolved value to determine the type of | | `context` | The context object | | `info` | GraphQL resolve info | ### Type Parameters `defineResolveType`: | Parameter | Description | |-----------|-------------| | `TAbstract` | The abstract type (union or interface) to resolve | ## Using isTypeOf Define an `isTypeOf` resolver on an object type to check if a value is of that type. ### Basic Usage ```typescript import { defineIsTypeOf } from "../gqlkit"; export interface Dog { kind: string; name: string; breed: string; } export interface Cat { kind: string; name: string; indoor: boolean; } export type Animal = Dog | Cat; export const dogIsTypeOf = defineIsTypeOf((value) => { return ( typeof value === "object" && value !== null && "kind" in value && value.kind === "dog" ); }); export const catIsTypeOf = defineIsTypeOf((value) => { return ( typeof value === "object" && value !== null && "kind" in value && value.kind === "cat" ); }); ``` ### Resolver Function Signature ```typescript (value: unknown, context: TContext, info: GraphQLResolveInfo) => boolean | Promise ``` | Argument | Description | |----------|-------------| | `value` | The value to check (typed as `unknown`) | | `context` | The context object | | `info` | GraphQL resolve info | ### Type Parameters `defineIsTypeOf`: | Parameter | Description | |-----------|-------------| | `TObject` | The object type to check against | --- # Documentation gqlkit extracts TSDoc/JSDoc comments and converts them to GraphQL descriptions. ## Type Descriptions Add descriptions to types using TSDoc/JSDoc comments: ```typescript /** * A user in the system */ export type User = { /** Unique identifier */ id: IDString; /** Display name */ name: string; }; ``` Generates: ```graphql """ A user in the system Defined in: src/gqlkit/schema/user.ts """ type User { """Unique identifier""" id: ID! """Display name""" name: String! } ``` ## Field Descriptions Add descriptions to individual fields: ```typescript export type User = { /** Unique identifier for the user */ id: IDString; /** * The user's display name. * This is shown in the UI and can be changed by the user. */ name: string; /** Email address (null if not verified) */ email: string | null; }; ``` ## Resolver Descriptions Add descriptions to Query, Mutation, and Field resolvers: ```typescript /** Get the currently authenticated user */ export const me = defineQuery( (_root, _args, ctx) => ctx.currentUser ); /** Create a new user account */ export const createUser = defineMutation<{ input: CreateUserInput }, User>( (_root, args, ctx) => ctx.db.createUser(args.input) ); /** Get user's profile URL */ export const profileUrl = defineField( (parent) => `https://example.com/users/${parent.id}` ); ``` ## @deprecated Directive Mark fields and enum values as deprecated using the `@deprecated` JSDoc tag: ### Deprecating Fields ```typescript export type User = { id: IDString; name: string; /** * Legacy username * @deprecated Use `name` field instead */ username: string | null; }; ``` Generates: ```graphql type User { id: ID! name: String! """Legacy username""" username: String @deprecated(reason: "Use `name` field instead") } ``` ### Deprecating Enum Values ```typescript export enum UserStatus { Active = "ACTIVE", /** * @deprecated Use `Inactive` instead */ Pending = "PENDING", Inactive = "INACTIVE", } ``` Generates: ```graphql enum UserStatus { ACTIVE PENDING @deprecated(reason: "Use `Inactive` instead") INACTIVE } ``` ### Deprecating Resolvers ```typescript /** * Get user by username * @deprecated Use `user(id: ID!)` instead */ export const userByUsername = defineQuery<{ username: string }, User | null>( (_root, args, ctx) => ctx.db.findUserByUsername(args.username) ); ``` ## Multi-line Descriptions Multi-line comments are preserved: ```typescript /** * Represents a blog post. * * Posts can be in draft or published state. * Only published posts are visible to other users. */ export type Post = { id: IDString; title: string; content: string; }; ``` Generates: ```graphql """ Represents a blog post. Posts can be in draft or published state. Only published posts are visible to other users. Defined in: src/gqlkit/schema/post.ts """ type Post { id: ID! title: String! content: String! } ``` --- # Custom Directives Define custom directives using the `GqlDirective` utility type and attach them using `GqlField` or `GqlObject`. ## Defining Directives ```typescript // src/gqlkit/schema/directives.ts import { type GqlDirective } from "@gqlkit-ts/runtime"; export type Role = "USER" | "ADMIN"; // Directive with typed arguments export type AuthDirective = GqlDirective< "auth", TArgs, "FIELD_DEFINITION" >; // Directive with multiple locations export type CacheDirective = GqlDirective< "cache", TArgs, ["FIELD_DEFINITION", "OBJECT"] >; ``` Generates: ```graphql directive @auth(role: [Role!]!) on FIELD_DEFINITION directive @cache(maxAge: Float!) on FIELD_DEFINITION | OBJECT ``` ## GqlDirective Type Parameters `GqlDirective`: | Parameter | Description | |-----------|-------------| | `Name` | The directive name (without `@`) | | `Args` | The directive arguments type | | `Location` | Where the directive can be used (single or array) | ### Directive Locations Common directive locations: | Location | Description | |----------|-------------| | `FIELD_DEFINITION` | Object type fields | | `OBJECT` | Object types | | `INPUT_FIELD_DEFINITION` | Input type fields | | `ARGUMENT_DEFINITION` | Field arguments | | `ENUM_VALUE` | Enum values | | `INTERFACE` | Interface types | ## Attaching Directives to Fields Use `GqlField` to attach directives to object type fields: ```typescript import { type GqlField, type IDString } from "@gqlkit-ts/runtime"; import { type AuthDirective } from "./directives.js"; export type User = { id: IDString; name: string; // Field with directive email: GqlField] }>; // Nullable field with directive phone: GqlField] }>; }; ``` Generates: ```graphql type User { id: ID! name: String! email: String! @auth(role: [ADMIN]) phone: String @auth(role: [USER]) } ``` ## Attaching Directives to Types Use `GqlObject` to attach directives to type definitions: ```typescript import { type GqlObject, type IDString } from "@gqlkit-ts/runtime"; import { type CacheDirective } from "./directives.js"; export type Post = GqlObject< { id: IDString; title: string; }, { directives: [CacheDirective<{ maxAge: 60 }>] } >; ``` Generates: ```graphql type Post @cache(maxAge: 60) { id: ID! title: String! } ``` ## Attaching Directives to Query/Mutation Fields Add a third type parameter to `defineQuery`, `defineMutation`, or `defineField`: ```typescript import { defineQuery } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import { type AuthDirective } from "./directives.js"; import type { User } from "./user.js"; // Query field with directive export const me = defineQuery< NoArgs, User, [AuthDirective<{ role: ["USER"] }>] >((_root, _args, ctx) => ctx.currentUser); ``` Generates: ```graphql type Query { me: User! @auth(role: [USER]) } ``` ## Attaching Directives to Field Resolvers ```typescript import { defineField } from "../gqlkit"; import type { NoArgs } from "@gqlkit-ts/runtime"; import { type AuthDirective } from "./directives.js"; export const email = defineField< User, NoArgs, string, [AuthDirective<{ role: ["ADMIN"] }>] >((parent) => parent.email); ``` ## Multiple Directives Attach multiple directives by adding them to the array: ```typescript export type Post = GqlObject< { id: IDString; title: string; }, { directives: [ CacheDirective<{ maxAge: 60 }>, AuthDirective<{ role: ["USER"] }> ] } >; ``` ## Combining with Other Options Combine directives with `implements` and `defaultValue`: ```typescript export type Post = GqlObject< { id: IDString; title: string; createdAt: DateTime; }, { implements: [Node, Timestamped], directives: [CacheDirective<{ maxAge: 60 }>] } >; export type CreatePostInput = { title: GqlField]; }>; }; ``` --- # graphql-yoga [graphql-yoga](https://the-guild.dev/graphql/yoga-server) is a batteries-included GraphQL server that works in any JavaScript runtime. ## Installation ```sh filename="npm" npm install graphql-yoga ``` ```sh filename="pnpm" pnpm add graphql-yoga ``` ```sh filename="yarn" yarn add graphql-yoga ``` ## Prerequisites Create an executable schema following the [Getting Started guide](../getting-started.md#create-graphql-schema). ## Basic Server ```typescript // src/server.ts import { createServer } from "node:http"; import { createYoga } from "graphql-yoga"; import { schema } from "./schema"; const yoga = createYoga({ schema }); const server = createServer(yoga); server.listen(4000, () => { console.log("Server is running on http://localhost:4000/graphql"); }); ``` ## With Context If your resolvers use a context type, first [set up context and resolver factories](../getting-started.md#set-up-context-and-resolver-factories), then provide a context factory to your server: ```typescript // src/server.ts import { createServer } from "node:http"; import { createYoga } from "graphql-yoga"; import { schema } from "./schema"; import type { Context } from "./gqlkit/context"; const yoga = createYoga<{}, Context>({ schema, context: async ({ request }) => { const user = await getUserFromRequest(request); return { currentUser: user, db: database, }; }, }); const server = createServer(yoga); server.listen(4000, () => { console.log("Server is running on http://localhost:4000/graphql"); }); ``` ## Further Reading - [graphql-yoga Documentation](https://the-guild.dev/graphql/yoga-server/docs) - [Envelop Plugins](https://the-guild.dev/graphql/envelop/plugins) --- # Apollo Server [Apollo Server](https://www.apollographql.com/docs/apollo-server/) is a popular GraphQL server with extensive features and ecosystem. ## Installation ```sh filename="npm" npm install @apollo/server ``` ```sh filename="pnpm" pnpm add @apollo/server ``` ```sh filename="yarn" yarn add @apollo/server ``` ## Prerequisites Create an executable schema following the [Getting Started guide](../getting-started.md#create-graphql-schema). ## Basic Server Using the standalone server: ```typescript // src/server.ts import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { schema } from "./schema"; const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`Server is running on ${url}`); ``` ## With Context If your resolvers use a context type, first [set up context and resolver factories](../getting-started.md#set-up-context-and-resolver-factories), then provide a context factory to your server: ```typescript // src/server.ts import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { schema } from "./schema"; import type { Context } from "./gqlkit/context"; const server = new ApolloServer({ schema }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async ({ req }) => { const user = await getUserFromRequest(req); return { currentUser: user, db: database, }; }, }); console.log(`Server is running on ${url}`); ``` ## Further Reading - [Apollo Server Documentation](https://www.apollographql.com/docs/apollo-server/) - [Apollo Server Plugins](https://www.apollographql.com/docs/apollo-server/builtin-plugins) --- # Drizzle ORM [Drizzle ORM](https://orm.drizzle.team/) is a TypeScript ORM with type-safe schema definitions. gqlkit integrates seamlessly with Drizzle by using `InferSelectModel` and `InferInsertModel` to derive GraphQL types from your table definitions. ## Installation ```sh filename="npm" npm install drizzle-orm postgres ``` ```sh filename="pnpm" pnpm add drizzle-orm postgres ``` ```sh filename="yarn" yarn add drizzle-orm postgres ``` ## Defining Tables Define your database tables with Drizzle: ```typescript // src/db/schema.ts import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; export const userStatusEnum = pgEnum("user_status", [ "active", "inactive", "suspended", ]); export const users = pgTable("users", { id: uuid().primaryKey().defaultRandom(), name: text().notNull(), email: text().notNull().unique(), status: userStatusEnum().notNull().default("active"), createdAt: timestamp().notNull().defaultNow(), }); export const posts = pgTable("posts", { id: uuid().primaryKey().defaultRandom(), title: text().notNull(), content: text(), priority: text({ enum: ["low", "medium", "high"] }).notNull().default("medium"), authorId: uuid() .notNull() .references(() => users.id), createdAt: timestamp().notNull().defaultNow(), }); ``` Both `pgEnum()` and `text({ enum: [...] })` are supported for defining enum columns. gqlkit automatically generates corresponding GraphQL enum types from these definitions. ## Defining Custom Scalars Define custom scalar types using `GqlScalar` for fields like timestamps: ```typescript // src/gqlkit/schema/scalars.ts import type { GqlScalar } from "@gqlkit-ts/runtime"; export type DateTime = GqlScalar<"DateTime", Date>; ``` ## Exporting GraphQL Types Use Drizzle's type inference utilities to export GraphQL types from your table definitions: ```typescript // src/gqlkit/schema/user.ts import type { InferSelectModel } from "drizzle-orm"; import { users as usersTable } from "../../db/schema.js"; // Export as GraphQL object type export type User = InferSelectModel; ``` This generates the following GraphQL schema: ```graphql enum UserStatus { ACTIVE INACTIVE SUSPENDED } type User { id: String! name: String! email: String! status: UserStatus! createdAt: DateTime! } ``` ## Defining Resolvers Define resolvers that use the derived types: ```typescript // src/gqlkit/schema/user.ts import type { NoArgs } from "@gqlkit-ts/runtime"; import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; import { eq } from "drizzle-orm"; import { posts as postsTable, users as usersTable } from "../../db/schema.js"; import { defineField, defineMutation, defineQuery } from "../gqlkit.js"; import type { Post } from "./post.js"; export type User = InferSelectModel; export const allUsers = defineQuery( async (_root, _args, ctx) => { return ctx.db.select().from(usersTable); }, ); export const user = defineQuery<{ id: string }, User | null>( async (_root, args, ctx) => { const result = await ctx.db .select() .from(usersTable) .where(eq(usersTable.id, args.id)); return result[0] ?? null; }, ); export const createUser = defineMutation< { input: Omit, "id" | "createdAt"> }, User >(async (_root, args, ctx) => { const result = await ctx.db.insert(usersTable).values(args.input).returning(); return result[0]!; }); export const posts = defineField( async (parent, _args, ctx) => { return ctx.db .select() .from(postsTable) .where(eq(postsTable.authorId, parent.id)); }, ); ``` ## Context with Database Set up the context type to include your database instance. For basic setup, see [Set Up Context and Resolver Factories](../getting-started.md#set-up-context-and-resolver-factories). ```typescript // src/db/db.ts import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema.js"; const client = postgres(process.env.DATABASE_URL!); export const db = drizzle(client, { schema, casing: "snake_case" }); export type Database = typeof db; ``` ```typescript // src/gqlkit/context.ts import type { Database } from "../db/db.js"; export type Context = { db: Database; }; ``` ## Complete Example See the [examples/with-drizzle](https://github.com/gqlkit/gqlkit/tree/main/examples/with-drizzle) directory for a complete working example with: - PostgreSQL tables with DateTime scalar - Enum types using both `pgEnum()` and `text({ enum: [...] })` - User and Post types with relationships - Query, Mutation, and Field resolvers ## Further Reading - [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) - [Drizzle with PostgreSQL](https://orm.drizzle.team/docs/get-started/postgresql-new) --- # Prisma [Prisma](https://www.prisma.io/) is a next-generation ORM for Node.js and TypeScript. gqlkit integrates seamlessly with Prisma by using its generated model types to derive GraphQL types from your schema definitions. ## Installation ```sh filename="npm" npm install prisma @prisma/client ``` ```sh filename="pnpm" pnpm add prisma @prisma/client ``` ```sh filename="yarn" yarn add prisma @prisma/client ``` ## Defining the Schema Define your database schema in `prisma/schema.prisma`: ```prisma // prisma/schema.prisma generator client { provider = "prisma-client" output = "../src/__generated__/prisma" } datasource db { provider = "sqlite" } enum UserStatus { active inactive suspended } model User { id String @id @default(uuid()) name String email String @unique status UserStatus @default(active) createdAt DateTime @default(now()) @map("created_at") posts Post[] @@map("users") } enum PostPriority { low medium high } model Post { id String @id @default(uuid()) title String content String? priority PostPriority @default(medium) authorId String @map("author_id") createdAt DateTime @default(now()) @map("created_at") author User @relation(fields: [authorId], references: [id]) @@map("posts") } ``` Enum types defined in the Prisma schema are automatically converted to corresponding GraphQL enum types by gqlkit. ## Defining Custom Scalars Define custom scalar types using `GqlScalar` for fields like timestamps: ```typescript // src/gqlkit/schema/scalars.ts import type { GqlScalar } from "@gqlkit-ts/runtime"; export type DateTime = GqlScalar<"DateTime", Date>; ``` ## Exporting GraphQL Types Use Prisma's generated model types to export GraphQL types from your schema definitions: ```typescript // src/gqlkit/schema/user.ts import type { Prisma } from "../../__generated__/prisma/client.js"; // Export as GraphQL object type export type User = Prisma.UserModel; ``` This generates the following GraphQL schema: ```graphql enum UserStatus { ACTIVE INACTIVE SUSPENDED } type User { id: String! name: String! email: String! status: UserStatus! createdAt: DateTime! } ``` ## Defining Resolvers Define resolvers that use the derived types: ```typescript // src/gqlkit/schema/user.ts import type { NoArgs } from "@gqlkit-ts/runtime"; import type { Prisma } from "../../__generated__/prisma/client.js"; import { defineField, defineMutation, defineQuery } from "../gqlkit.js"; import type { Post } from "./post.js"; export type User = Prisma.UserModel; export const allUsers = defineQuery( async (_root, _args, ctx) => { return ctx.db.user.findMany(); }, ); export const user = defineQuery<{ id: string }, User | null>( async (_root, args, ctx) => { return ctx.db.user.findUnique({ where: { id: args.id }, }); }, ); export const createUser = defineMutation< { input: Omit }, User >(async (_root, args, ctx) => { return ctx.db.user.create({ data: args.input, }); }); export const posts = defineField( async (parent, _args, ctx) => { return ctx.db.post.findMany({ where: { authorId: parent.id }, }); }, ); ``` ## Context with Database Set up the context type to include your Prisma client instance. For basic setup, see [Set Up Context and Resolver Factories](../getting-started.md#set-up-context-and-resolver-factories). ```typescript // src/db/db.ts import { PrismaClient } from "../__generated__/prisma/client.js"; export const prisma = new PrismaClient(); export type PrismaDatabase = typeof prisma; ``` ```typescript // src/gqlkit/context.ts import type { PrismaDatabase } from "../db/db.js"; export type Context = { db: PrismaDatabase; }; ``` ## Complete Example See the [examples/with-prisma](https://github.com/gqlkit/gqlkit/tree/main/examples/with-prisma) directory for a complete working example with: - SQLite database with DateTime scalar - Enum types (`UserStatus`, `PostPriority`) - User and Post types with relationships - Query, Mutation, and Field resolvers ## Further Reading - [Prisma Documentation](https://www.prisma.io/docs) - [Prisma Client API Reference](https://www.prisma.io/docs/orm/reference/prisma-client-reference) --- # Vercel AI SDK The [Vercel AI SDK](https://ai-sdk.dev/) provides a unified API for working with large language models. gqlkit integrates with the AI SDK by automatically converting its rich message types — including tool invocations, step boundaries, and custom data parts — into GraphQL unions and subscriptions. ## Installation ```sh filename="npm" npm install ai @ai-sdk/openai ``` ```sh filename="pnpm" pnpm add ai @ai-sdk/openai ``` ```sh filename="yarn" yarn add ai @ai-sdk/openai ``` ## Defining Tools Define AI tools using the AI SDK's `tool()` helper. The `ToolSet` and `InferUITools` types let you derive strongly-typed UI tool representations: ```typescript // src/agent.ts import type { InferUITools, ToolSet, UIMessage } from "ai"; import { tool } from "ai"; import { z } from "zod"; export const tools = { weather: tool({ description: "Get current weather for a location", inputSchema: z.object({ location: z.string().describe("City name"), }), execute: async ({ location }) => ({ location, temperature: 72, condition: "sunny" as const, }), }), calculate: tool({ description: "Calculate a math expression", inputSchema: z.object({ expression: z.string().describe("Math expression"), }), execute: async ({ expression }) => ({ expression, result: 42, }), }), } satisfies ToolSet; export type AppTools = InferUITools; export type AppMetadata = { model: string; timestamp: number; }; export type AppData = { chart: { labels: string[]; values: number[] }; suggestion: { text: string; confidence: number }; }; export type AppMessage = UIMessage; ``` ## Defining the Message Type Re-export your `AppMessage` as a named type in the gqlkit schema directory. gqlkit discovers the `UIMessage` fields — including the `parts` union — transitively, so you don't need to manually define each part type: ```typescript // src/gqlkit/schema/message.ts import type { NoArgs } from "@gqlkit-ts/runtime"; import type { AppMessage } from "../../agent.js"; import { defineQuery } from "../gqlkit.js"; // Re-export AppMessage as Message — gqlkit discovers UIMessage fields transitively. export type Message = AppMessage; export const messages = defineQuery(() => mockMessages); export const message = defineQuery<{ id: string }, Message | null>( (_root, args) => mockMessages.find((m) => m.id === args.id) ?? null, ); ``` The `parts` field on `UIMessage` is a discriminated union of text parts, tool invocations, tool results, step boundaries, source parts, file parts, reasoning parts, and your custom data parts. gqlkit automatically generates a corresponding GraphQL union type for this. ## Defining Subscriptions The AI SDK provides two streaming patterns that map naturally to GraphQL subscriptions: ### Raw Stream (chunk-by-chunk) Use `toUIMessageStream()` to yield each `UIMessageChunk` as it arrives: ```typescript // src/gqlkit/schema/subscription.ts import type { InferUIMessageChunk } from "ai"; import { streamText } from "ai"; import type { AppMessage } from "../../agent.js"; import { tools } from "../../agent.js"; import { defineSubscription } from "../gqlkit.js"; export type ChatStreamChunk = InferUIMessageChunk; export const chatStream = defineSubscription< { prompt: string }, ChatStreamChunk >(async (_root, args, ctx) => { const result = streamText({ model: ctx.model, prompt: args.prompt, tools }); return result.toUIMessageStream(); }); ``` ### Assembled Message (progressive updates) Use `readUIMessageStream()` to yield the progressively-built `Message` on each update: ```typescript import { readUIMessageStream, streamText } from "ai"; import type { AppMessage } from "../../agent.js"; import { tools } from "../../agent.js"; import { defineSubscription } from "../gqlkit.js"; import type { Message } from "./message.js"; export const chat = defineSubscription<{ prompt: string }, Message>( async (_root, args, ctx) => { const result = streamText({ model: ctx.model, prompt: args.prompt, tools }); return readUIMessageStream({ stream: result.toUIMessageStream(), }); }, ); ``` ## Discriminator Fields Configuration The AI SDK's union types use discriminator fields that gqlkit needs to know about. Configure them in `gqlkit.config.ts`: ```typescript // gqlkit.config.ts import type { GqlkitConfig } from "@gqlkit-ts/cli"; const config: GqlkitConfig = { discriminatorFields: { // Message.parts union — discriminated by "type" with "state" as a secondary field MessageParts: ["type", "state"], // UIMessageChunk variants — discriminated by "type" ChatStreamChunk: ["type"], }, }; export default config; ``` The `discriminatorFields` option tells gqlkit which fields to use when generating the GraphQL union's `__resolveType` implementation. ## Context with Language Model Inject the `LanguageModel` via context so resolvers stay testable and model-agnostic. For basic setup, see [Set Up Context and Resolver Factories](../getting-started.md#set-up-context-and-resolver-factories). ```typescript // src/gqlkit/context.ts import type { LanguageModel } from "ai"; export type Context = { model: LanguageModel; }; ``` ## Complete Example See the [examples/with-ai-sdk](https://github.com/gqlkit/gqlkit/tree/main/examples/with-ai-sdk) directory for a complete working example with: - Tool definitions with Zod schemas - `UIMessage` with custom metadata, data parts, and tool types - Both streaming patterns (`chatStream` and `chat` subscriptions) - Discriminator fields configuration ## Further Reading - [AI SDK Documentation](https://ai-sdk.dev/docs) - [AI SDK GitHub](https://github.com/vercel/ai) --- # Configuration gqlkit can be configured via `gqlkit.config.ts` in your project root. ## CLI Options The `gqlkit gen` command accepts the following options: | Option | Short | Description | |--------|-------|-------------| | `--config ` | `-c` | Path to config file (default: `gqlkit.config.ts`) | | `--cwd ` | | Working directory for code generation | ### Using Multiple Config Files You can use different config files for different GraphQL schemas: ```sh # Generate with default config (gqlkit.config.ts) gqlkit gen # Generate with a specific config file gqlkit gen --config gqlkit.public.config.ts gqlkit gen -c gqlkit.admin.config.ts ``` This is useful when you need to generate multiple GraphQL schemas from the same project, such as separate public and internal APIs. ## Basic Configuration ```ts // gqlkit.config.ts import { defineConfig } from "@gqlkit-ts/cli"; export default defineConfig({ // Source directory (default: "src/gqlkit/schema") sourceDir: "src/gqlkit/schema", // Glob patterns to exclude from scanning sourceIgnoreGlobs: ["**/*.test.ts"], // Custom scalar mappings (config-based approach) scalars: [ { name: "DateTime", tsType: { from: "./src/gqlkit/schema/scalars", name: "DateTime" }, description: "ISO 8601 datetime", }, { name: "UUID", tsType: { name: "string" }, // Global type }, ], // Output paths output: { typeDefsPath: "src/gqlkit/__generated__/typeDefs.ts", resolversPath: "src/gqlkit/__generated__/resolvers.ts", schemaPath: "src/gqlkit/__generated__/schema.graphql", // Set to null to disable output: // schemaPath: null, // Import extension in generated files (default: "js") // importExtension: "none", // for bundlers // Keep types unreachable from root operation types (default: true) // pruning: false, }, // Hooks hooks: { // Run after all files are written (e.g., formatting) afterAllFileWrite: "prettier --write", // Or multiple commands: // afterAllFileWrite: ["prettier --write", "eslint --fix"], }, }); ``` ## Custom Scalar Types Map TypeScript types to GraphQL custom scalars via config. See [Scalars](./schema/scalars) for complete documentation including the `GqlScalar` approach. ```ts export default defineConfig({ scalars: [ { name: "DateTime", tsType: { from: "./src/gqlkit/schema/scalars", name: "DateTime" }, description: "ISO 8601 datetime string", }, ], }); ``` ## Output Paths Customize output file locations or disable specific outputs: ```ts export default defineConfig({ output: { typeDefsPath: "generated/schema.ts", // Custom path resolversPath: "generated/resolvers.ts", schemaPath: null, // Disable SDL output }, }); ``` ### Output Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `typeDefsPath` | `string` \| `null` | `"src/gqlkit/__generated__/typeDefs.ts"` | Path for the TypeDefs output | | `resolversPath` | `string` \| `null` | `"src/gqlkit/__generated__/resolvers.ts"` | Path for the resolvers output | | `schemaPath` | `string` \| `null` | `"src/gqlkit/__generated__/schema.graphql"` | Path for the SDL output | | `importExtension` | `"js"` \| `"none"` \| `"ts"` | `"js"` | File extension for import statements | | `pruning` | `boolean` | `true` | Remove types unreachable from root operation types | Set any path to `null` to disable that output. The `importExtension` option controls file extensions in generated import statements: - `"js"` (default): Converts `.ts` to `.js` (compatible with ESM + Node16) - `"none"`: No extension (for bundlers like webpack, vite) - `"ts"`: Keeps `.ts` extension (for Deno) ## Schema Pruning By default, gqlkit removes types that are not reachable from any root operation type (`Query`, `Mutation`, `Subscription`) from the generated schema, typeDefs, and resolver map. Reachability follows field types, argument types, input object fields, union members, and `implements` references; every type implementing a reachable interface is also kept, since `__resolveType` may return any implementer at runtime. Pruning never removes: - **Directive definitions** — all directive definitions (and the types their arguments reference) are always kept. - **Anything in a schema without root operation types** — if no `Query`, `Mutation`, or `Subscription` exists (a types-only project), pruning is skipped entirely so the output is not emptied. When types are pruned, `gqlkit gen` reports them: ``` Pruned 2 unused type(s): AuditLog, LegacyInput ``` To keep every extracted type in the output, opt out: ```ts export default defineConfig({ output: { pruning: false, }, }); ``` ## Hooks Execute commands after file generation: ```ts export default defineConfig({ hooks: { // Single command afterAllFileWrite: "prettier --write", }, }); ``` You can also specify multiple commands to run sequentially: ```ts export default defineConfig({ hooks: { // Multiple commands (executed sequentially) afterAllFileWrite: ["prettier --write", "eslint --fix"], }, }); ``` ### Available Hooks | Hook | Description | |------|-------------| | `afterAllFileWrite` | Runs after all generated files are written. Receives the list of written file paths. | ## Custom Discriminator Fields Specify custom TypeScript fields for union type resolution. This is useful when union members use a discriminator field other than `__typename` or `$typeName` (e.g., external library types). ```ts export default defineConfig({ discriminatorFields: { // Single field ContentPart: "type", // Multiple fields for tuple-based discrimination Content: ["type", "mediaType"], }, }); ``` Keys are GraphQL union type names, and values are either a single field name or an array of field names. gqlkit automatically generates `__resolveType` functions based on these fields. See [Union Types - Custom Discriminator Fields](./schema/unions.md#custom-discriminator-fields) for detailed usage and validation rules. ## Source Directory By default, gqlkit scans `src/gqlkit/schema` for types and resolvers. ```ts export default defineConfig({ sourceDir: "src/graphql/schema", // Custom source directory }); ``` All TypeScript files (`.ts`, `.cts`, `.mts`) under this directory are scanned. ## Ignore Patterns Exclude files from scanning using glob patterns: ```ts export default defineConfig({ sourceIgnoreGlobs: [ "**/*.test.ts", "**/*.spec.ts", "**/testdata/**", ], }); ``` --- # Coding Agents gqlkit provides built-in support for AI coding agents like [Claude Code](https://claude.ai/code) and [OpenAI Codex](https://openai.com/index/openai-codex/). The `gqlkit docs` command generates skill files that help these tools understand gqlkit conventions and provide better assistance. ## Setup Run the following command in your project root: ```sh filename="npm" npm exec gqlkit docs ``` ```sh filename="pnpm" pnpm gqlkit docs ``` ```sh filename="yarn" yarn gqlkit docs ``` The command auto-detects your environment based on existing files (`CLAUDE.md`, `.claude/`, `AGENTS.md`, `.codex/`) and generates the appropriate files. ## Generated Files ### Claude Code When Claude Code environment is detected (or `--claude` flag is used): - `.claude/skills/gqlkit-guide/SKILL.md` - Skill definition with gqlkit documentation - `.claude/skills/gqlkit-guide/references/` - Symlink to detailed documentation - `CLAUDE.md` - Updated with gqlkit section (created if not exists) ### OpenAI Codex When Codex environment is detected (or `--codex` flag is used): - `.codex/skills/gqlkit-guide/SKILL.md` - Skill definition with gqlkit documentation - `.codex/skills/gqlkit-guide/references/` - Symlink to detailed documentation - `AGENTS.md` - Updated with gqlkit section (created if not exists) ## Options | Option | Description | |--------|-------------| | `--output ` | Output directory (default: current directory) | | `--claude` | Generate Claude Code files explicitly | | `--codex` | Generate Codex files explicitly | ## How It Works The generated skill files follow the [Agent Skills](https://agentskills.io/) specification. When you ask the coding agent about gqlkit, it will: 1. Recognize gqlkit-related questions from the skill description 2. Read the bundled documentation to understand gqlkit conventions 3. Provide accurate guidance based on the latest documentation ## Keeping Skills Updated Re-run `gqlkit docs` after updating gqlkit to ensure the skill files reflect the latest documentation. ---