| name | graphql-suite |
| description | Build GraphQL APIs from Drizzle PostgreSQL schemas with auto-generated CRUD, type-safe clients, and React Query hooks. Use when creating GraphQL servers from Drizzle ORM tables, building type-safe GraphQL clients, adding React data-fetching hooks with TanStack Query, or generating GraphQL SDL/types from Drizzle schemas. Use when this capability is needed. |
| metadata | {"author":"annexare"} |
graphql-suite
Three-layer toolkit that turns Drizzle ORM PostgreSQL schemas into fully working GraphQL APIs with end-to-end type safety.
Packages
| Import | Package | Purpose |
|---|
graphql-suite/schema | @graphql-suite/schema | Server-side GraphQL schema builder with CRUD, filtering, hooks, and codegen |
graphql-suite/client | @graphql-suite/client | Type-safe GraphQL client with entity-based API |
graphql-suite/query | @graphql-suite/query | TanStack React Query hooks wrapping the client |
Data flow: Drizzle schema -> buildSchema() -> GraphQL server -> createDrizzleClient() -> <GraphQLProvider> + hooks
Peer dependencies:
./schema: drizzle-orm >=0.44.0, graphql >=16.3.0
./client: drizzle-orm >=0.44.0
./query: react >=18.0.0, @tanstack/react-query >=5.0.0
When to Use
Use this skill when the user is:
- Creating a GraphQL server from Drizzle ORM table definitions
- Building type-safe GraphQL clients for a graphql-suite server
- Adding React data-fetching with TanStack Query for GraphQL
- Generating GraphQL SDL or static TypeScript types when client and server are in separate repos
- Configuring hooks, table exclusion, relation depth, or operation filtering
- Setting up runtime permissions or role-based schema variants
- Implementing row-level security with WHERE clause injection
- Working with relation-level filtering (some/every/none quantifiers)
- Debugging GraphQL schema generation or client type inference
Quick Start
1. Define Drizzle Schema
import { relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
export const user = pgTable('user', {
id: uuid().primaryKey().defaultRandom(),
name: text().notNull(),
email: text().notNull(),
})
export const post = pgTable('post', {
id: uuid().primaryKey().defaultRandom(),
title: text().notNull(),
body: text().notNull(),
userId: uuid().notNull(),
})
export const userRelations = relations(user, ({ many }) => ({
posts: many(post),
}))
export const postRelations = relations(post, ({ one }) => ({
author: (user, { : [post.], : [user.] }),
}))
2. Build GraphQL Server
import { buildSchema } from '@graphql-suite/schema'
import { createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { db } from './db'
const { schema } = buildSchema(db, {
tables: { exclude: ['session'] },
hooks: {
user: {
query: {
before: async ({ context }) => {
if (!context.user) throw new Error('Unauthorized')
},
},
},
},
})
const yoga = createYoga({ schema })
createServer(yoga).listen(4000)
3. Create Type-Safe Client
import { createDrizzleClient } from '@graphql-suite/client'
import * as schema from './db/schema'
const client = createDrizzleClient({
schema,
config: { suffixes: { list: 's' } },
url: '/api/graphql',
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
})
const users = await client.entity('user').query({
select: { id: true, name: true, posts: { id: true, title: true } },
where: { name: { ilike: '%john%' } },
limit: 10,
})
4. Add React Hooks
import { GraphQLProvider, useEntity, useEntityList } from '@graphql-suite/query'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<GraphQLProvider client={graphqlClient}>
<UserList />
</GraphQLProvider>
</QueryClientProvider>
)
}
function UserList() {
const user = useEntity('user')
const { data, isLoading } = useEntityList(user, {
select: { id: true, name: true, email: true },
limit: 20,
})
if (isLoading) return <div>Loading...</div>
}
Core API
Schema Package (graphql-suite/schema)
buildSchema(db, config?): { schema: GraphQLSchema; entities: GeneratedEntities; withPermissions: (p: PermissionConfig) => GraphQLSchema }
buildEntities(db, config?): GeneratedEntities
buildSchemaFromDrizzle(drizzleSchema, config?): { schema: GraphQLSchema; entities: GeneratedEntities; withPermissions: (p: PermissionConfig) => GraphQLSchema }
permissive(id, tables?): PermissionConfig
restricted(id, tables?): PermissionConfig
readOnly(): TableAccess
withRowSecurity(rules): HooksConfig
mergeHooks(...configs):
(schema):
(schema, options?):
(schema, options?):
Client Package (graphql-suite/client)
createDrizzleClient(options): GraphQLClient
createClient(config): GraphQLClient
buildSchemaDescriptor(schema, config?): SchemaDescriptor
GraphQLClientError
NetworkError
Entity operations (via client.entity('name')):
| Method | Description |
|---|
query({ select, where?, limit?, offset?, orderBy? }) | List query returning T[] |
querySingle({ select, where?, offset?, orderBy? }) | Single query returning T | null |
count({ where? }) | Count matching rows |
insert({ values, returning? }) | Insert array, returns T[] |
insertSingle({ values, returning? }) | Insert one, returns T | null |
update({ set, where?, returning? }) | Update matching rows |
delete({ where?, returning? }) | Delete matching rows |
Query Package (graphql-suite/query)
<GraphQLProvider client={graphqlClient}>
useGraphQLClient()
useEntity(entityName)
useEntityQuery(entity, params, options?)
useEntityList(entity, params, options?)
useEntityInfiniteQuery(entity, params, options?)
useEntityInsert(entity, returning?, options?)
useEntityUpdate(entity, returning?, options?)
useEntityDelete(entity, returning?, options?)
Configuration
BuildSchemaConfig (Server)
buildSchema(db, {
mutations: true,
limitRelationDepth: 3,
limitSelfRelationDepth: 1,
suffixes: { list: '', single: 'Single' },
tables: {
exclude: ['session', 'migration'],
config: {
auditLog: { queries: true, mutations: false },
},
},
pruneRelations: {
'user.sensitiveData': false,
'post.comments': 'leaf',
'org.members': { only: ['profile'] },
},
hooks: { },
debug: true,
})
ClientSchemaConfig (Client)
The client config must align with the server config for correct query generation:
createDrizzleClient({
schema,
config: {
mutations: true,
suffixes: { list: 's', single: 'Single' },
tables: { exclude: ['session'] },
pruneRelations: { 'user.secret': false },
},
url: '/api/graphql',
})
See references/configuration.md for full details.
Hooks
Hooks intercept query/mutation execution on the server. Two patterns:
Before/After Hooks
hooks: {
user: {
query: {
before: async ({ args, context, info }) => {
if (!context.user) throw new Error('Unauthorized')
},
after: async ({ result, beforeData, context }) => {
return result
},
},
},
}
Resolve Hooks (replace entire resolver)
hooks: {
post: {
insert: {
resolve: async ({ args, context, info, defaultResolve }) => {
args.values = args.values.map((v) => ({ ...v, authorId: context.user.id }))
return defaultResolve(args)
},
},
},
}
Hooks apply to all 7 operation types: query, querySingle, count, insert, insertSingle, update, delete.
See patterns/hooks-patterns.md for common recipes.
Permissions
Build filtered GraphQLSchema variants per role or user — introspection fully reflects what each role can see and do.
import { buildSchema, permissive, restricted, readOnly } from '@graphql-suite/schema'
const { schema, withPermissions } = buildSchema(db)
const adminSchema = schema
const maintainerSchema = withPermissions(
permissive('maintainer', { audit: false, users: readOnly() }),
)
const userSchema = withPermissions(
restricted('user', { posts: { query: true }, comments: { query: true } }),
)
const anonSchema = withPermissions(restricted('anon'))
Permission Helpers
| Helper | Description |
|---|
permissive(id, tables?) | All tables allowed by default; overrides deny |
restricted(id, tables?) | Nothing allowed by default; overrides grant |
readOnly() | Shorthand for { query: true, insert: false, update: false, delete: false } |
TableAccess
Each table can be set to true (all operations), false (excluded entirely), or a TableAccess object:
type TableAccess = {
query?: boolean
insert?: boolean
update?: boolean
delete?: boolean
}
In permissive mode, omitted fields default to true. In restricted mode, omitted fields default to false.
Caching
Schemas are cached by id — calling withPermissions with the same id returns the same GraphQLSchema instance.
See references/permissions.md for full API details and examples/permissions.md for multi-role examples.
Row-Level Security
Generate hooks that inject WHERE clauses for row-level filtering. Compose with other hooks using mergeHooks.
import { buildSchema, withRowSecurity, mergeHooks } from '@graphql-suite/schema'
const { schema } = buildSchema(db, {
hooks: mergeHooks(
withRowSecurity({
posts: (context) => ({ authorId: { eq: context.user.id } }),
}),
myOtherHooks,
),
})
withRowSecurity(rules)
Generates a HooksConfig with before hooks on query, querySingle, count, update, and delete operations. Each rule is a function that receives the GraphQL context and returns a WHERE filter object.
mergeHooks(...configs)
Deep-merges multiple HooksConfig objects:
before hooks — chained sequentially; each receives the previous hook's modified args
after hooks — chained sequentially; each receives the previous hook's result
resolve hooks — last one wins (cannot be composed)
See patterns/hooks-patterns.md for composition recipes.
Relation Filtering
Filter across relations using EXISTS subqueries:
where: { author: { name: { eq: 'Alice' } } }
where: {
comments: {
some: { body: { ilike: '%bug%' } },
every: { approved: { eq: true } },
none: { spam: { eq: true } },
},
}
where: {
OR: [
{ title: { ilike: '%graphql%' } },
{ author: { name: { eq: 'Alice' } } },
],
}
Error Handling
Server (Schema Package)
Builder/config validation errors are prefixed with "GraphQL-Suite Error: ...".
Errors thrown in hooks or resolvers are caught and re-thrown as GraphQLError with the original message (no prefix added):
Client
import { GraphQLClientError, NetworkError } from '@graphql-suite/client'
try {
await client.entity('user').query({ select: { id: true } })
} catch (e) {
if (e instanceof NetworkError) {
console.error('HTTP error:', e.status, e.message)
}
if (e instanceof GraphQLClientError) {
console.error('GraphQL errors:', e.errors)
console.error('HTTP status:', e.status)
}
}
Generated Operation Names
Table user | Generated Name |
|---|
| List query | user (or users with suffixes.list: 's') |
| Single query | userSingle (customizable via suffixes.single) |
| Count query | userCount |
| Insert | insertIntoUser |
| Insert single | insertIntoUserSingle |
| Update | updateUser |
| Delete | deleteFromUser |
Resources
Reference Documentation
- Schema API — Full schema package API with all function signatures
- Client API — Client package API, EntityClient methods, error classes
- Query API — React hooks API, options, cache invalidation
- Configuration — BuildSchemaConfig and ClientSchemaConfig details
- Permissions — Permission helpers, withPermissions, TableAccess, RLS, mergeHooks
- Type Mapping — PostgreSQL column to GraphQL type mapping
- Code Generation — SDL/type generation for separate-repo setups
Examples
Patterns
Source: annexare/graphql-suite — distributed by TomeVault.