skills/graphql-suite/SKILL.md
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.
npx skillsauth add annexare/drizzle-graphql-suite graphql-suiteInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Three-layer toolkit that turns Drizzle ORM PostgreSQL schemas into fully working GraphQL APIs with end-to-end type safety.
| 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.0Use this skill when the user is:
// db/schema.ts
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: one(user, { fields: [post.userId], references: [user.id] }),
}))
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)
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,
})
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>
return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}
graphql-suite/schema)// Build complete GraphQL schema from Drizzle db instance
buildSchema(db, config?): { schema: GraphQLSchema; entities: GeneratedEntities; withPermissions: (p: PermissionConfig) => GraphQLSchema }
// Build entities only (queries, mutations, inputs, types) without full schema
buildEntities(db, config?): GeneratedEntities
// Build schema from Drizzle exports without a db connection (for codegen only)
buildSchemaFromDrizzle(drizzleSchema, config?): { schema: GraphQLSchema; entities: GeneratedEntities; withPermissions: (p: PermissionConfig) => GraphQLSchema }
// Permission helpers — build PermissionConfig objects for withPermissions()
permissive(id, tables?): PermissionConfig // All tables allowed by default; overrides deny
restricted(id, tables?): PermissionConfig // Nothing allowed by default; overrides grant
readOnly(): TableAccess // Shorthand: queries only, no mutations
// Row-level security and hook composition
withRowSecurity(rules): HooksConfig // Generate WHERE-injecting hooks from rules
mergeHooks(...configs): HooksConfig // Deep-merge multiple HooksConfig objects
// Code generation (for separate-repo setups where client can't import Drizzle schema)
generateSDL(schema): string // GraphQL SDL string
generateTypes(schema, options?): string // TypeScript types (wire, filters, inputs, orderBy)
generateEntityDefs(schema, options?): string // Runtime entity descriptors + EntityDefs type
// Custom scalar
GraphQLJSON // JSON scalar type for json/jsonb columns
graphql-suite/client)// Recommended: create client from Drizzle schema (full type inference)
createDrizzleClient(options): GraphQLClient
// Alternative: create client from pre-generated schema descriptor
createClient(config): GraphQLClient
// Build schema descriptor from Drizzle schema (for codegen workflows)
buildSchemaDescriptor(schema, config?): SchemaDescriptor
// Error classes
GraphQLClientError // GraphQL response errors (has .errors, .status)
NetworkError // HTTP/network failures (has .status)
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 |
graphql-suite/query)// Provider (wrap app)
<GraphQLProvider client={graphqlClient}>
// Access hooks
useGraphQLClient() // Get client from context
useEntity(entityName) // Get typed EntityClient
// Query hooks
useEntityQuery(entity, params, options?) // Single entity (T | null)
useEntityList(entity, params, options?) // List query (T[])
useEntityInfiniteQuery(entity, params, options?) // Paginated infinite query
// Mutation hooks
useEntityInsert(entity, returning?, options?) // Insert mutation
useEntityUpdate(entity, returning?, options?) // Update mutation
useEntityDelete(entity, returning?, options?) // Delete mutation
BuildSchemaConfig (Server)buildSchema(db, {
mutations: true, // Generate mutations (default: true)
limitRelationDepth: 3, // Max relation nesting depth (default: 3, 0 = no relations)
limitSelfRelationDepth: 1, // Self-relation depth (default: 1 = omitted)
suffixes: { list: '', single: 'Single' }, // Query name suffixes
tables: {
exclude: ['session', 'migration'], // Omit tables entirely
config: {
auditLog: { queries: true, mutations: false }, // Per-table operation control
},
},
pruneRelations: {
'user.sensitiveData': false, // Omit relation entirely
'post.comments': 'leaf', // Expand with scalars only
'org.members': { only: ['profile'] }, // Expand with listed relations only
},
hooks: { /* see Hooks section */ },
debug: true, // Log schema size diagnostics
})
ClientSchemaConfig (Client)The client config must align with the server config for correct query generation:
createDrizzleClient({
schema,
config: {
mutations: true, // Must match server
suffixes: { list: 's', single: 'Single' }, // Must match server
tables: { exclude: ['session'] }, // Must match server
pruneRelations: { 'user.secret': false }, // Must match server
},
url: '/api/graphql',
})
See references/configuration.md for full details.
Hooks intercept query/mutation execution on the server. Two patterns:
hooks: {
user: {
query: {
before: async ({ args, context, info }) => {
if (!context.user) throw new Error('Unauthorized')
// Optionally return { args } to override resolver arguments
// Optionally return { data } to pass data to after hook
},
after: async ({ result, beforeData, context }) => {
// Transform or log result
return result
},
},
},
}
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.
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)
// Full schema (admin)
const adminSchema = schema
// Permissive: everything allowed except audit (excluded) and users (read-only)
const maintainerSchema = withPermissions(
permissive('maintainer', { audit: false, users: readOnly() }),
)
// Restricted: nothing allowed except posts and comments (queries only)
const userSchema = withPermissions(
restricted('user', { posts: { query: true }, comments: { query: true } }),
)
// Restricted with nothing granted — only Query { _empty: Boolean }
const anonSchema = withPermissions(restricted('anon'))
| 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 } |
TableAccessEach table can be set to true (all operations), false (excluded entirely), or a TableAccess object:
type TableAccess = {
query?: boolean // list + single + count
insert?: boolean // insert + insertSingle
update?: boolean
delete?: boolean
}
In permissive mode, omitted fields default to true. In restricted mode, omitted fields default to false.
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.
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 argsafter hooks — chained sequentially; each receives the previous hook's resultresolve hooks — last one wins (cannot be composed)See patterns/hooks-patterns.md for composition recipes.
Filter across relations using EXISTS subqueries:
// One-to-one: direct filter
where: { author: { name: { eq: 'Alice' } } }
// One-to-many: quantifier object
where: {
comments: {
some: { body: { ilike: '%bug%' } }, // At least one comment matches
every: { approved: { eq: true } }, // All comments match
none: { spam: { eq: true } }, // No comments match
},
}
// Logical OR
where: {
OR: [
{ title: { ilike: '%graphql%' } },
{ author: { name: { eq: 'Alice' } } },
],
}
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):
// Config errors: "GraphQL-Suite Error: List and single query suffixes cannot be the same."
// Resolver/hook errors: re-thrown as GraphQLError(e.message) — original message preserved
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) // GraphQLErrorEntry[]
console.error('HTTP status:', e.status)
}
}
| 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 |
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.