Index of every page on this site: llms.txt
All skills
annexare avatar

/graphql-suite

@17a4240
Run it yourself

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 this Skill: https://skilld.dev/gh/annexare/graphql-suite/graphql-suite

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

SKILL.md

≈84 tokens for metadata: the name and description. ≈4.2k when used: this file. ≈24k more on demand in 18 files.

Description uses 4.2% of example budget

Before choosing a Skill, your Agent reads its name and description. All available Skills share that space.

  • A shorter description leaves more room for other Skills. This entry exceeds our 1% size suggestion.

In our Claude Code example, all Skill names and descriptions share 8,000 characters. This Skill uses ≈337 characters, or 4.2%.

The 1% threshold is a size suggestion. Longer descriptions can still fit.

Your model, settings, and other Skills decide how much text your Agent can read.

Example settings and source

The example uses a 200k-token context and default Claude Code settings. The count includes the name, description, separators, and when_to_use when present. Codex also counts local file paths.

Skit's source and limits: Codex 0.160.1, Claude Code 2.1.292.

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.4.0 || ^17.0.0 (graphql 17 needs Node 22+, and graphql-yoga >=5.22.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

// 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] }),
}))

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>
  return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}

Core API

Schema Package (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

// Resolver utilities
parseResolveInfo(info): ResolveTree | null            // Selection tree for the field being resolved

Client Package (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

Query Package (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

Configuration

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

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')
        // 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
      },
    },
  },
}

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)

// 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'))

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   // 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.

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:

// 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' } } },
  ],
}

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):

// 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

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)  // GraphQLErrorEntry[]
    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: SKILL.md on GitHub

No rule matched.

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

No alerts7mo3 checks · Risk SAFE
  • Gen Agent Trust Hub7mo

    The graphql-suite skill is a developer toolkit designed to bridge Drizzle ORM with GraphQL APIs. It provides comprehensive documentation and examples for schema generation, client creation, and React integration. No malicious patterns, obfuscation, or unauthorized data access were detected.

  • Socket7mo

    No alerts

  • Snyk7mo

    Risk: LOW · No issues

Signed by skilld at 17a4240. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago

README badge

README badge for annexare/graphql-suite