JSPM

  • Created
  • Published
  • Downloads 1706
  • Score
    100M100P100Q133920F
  • License MIT

GraphQL SDK generator for Constructive databases with React Query hooks

Package Exports

  • @constructive-io/graphql-codegen
  • @constructive-io/graphql-codegen/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@constructive-io/graphql-codegen) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@constructive-io/graphql-codegen

GraphQL SDK generator for Constructive databases. Generate type-safe React Query hooks or a Prisma-like ORM client from your GraphQL schema.

Features

  • Two Output Modes: React Query hooks OR Prisma-like ORM client
  • Full Schema Coverage: Generates code for ALL queries and mutations, not just table CRUD
  • PostGraphile Optimized: Uses _meta query for table metadata and __schema introspection for custom operations
  • React Query Integration: Generates useQuery and useMutation hooks with proper typing
  • Prisma-like ORM: Fluent API with db.user.findMany(), db.mutation.login(), etc.
  • Advanced Type Inference: Const generics for narrowed return types based on select clauses
  • Relation Support: Typed nested selects for belongsTo, hasMany, and manyToMany relations
  • Error Handling: Discriminated unions with .unwrap(), .unwrapOr(), .unwrapOrElse() methods
  • AST-Based Generation: Uses Babel for reliable code generation
  • Configurable: Filter tables, queries, and mutations with glob patterns
  • Type-Safe: Full TypeScript support with generated interfaces

Table of Contents

Installation

pnpm add @constructive-io/graphql-codegen

Programmatic API

The primary way to use this package is through the programmatic API. Import the generate function and call it with your configuration.

Generate from Endpoint

import { generate } from '@constructive-io/graphql-codegen';

// Generate React Query hooks from a GraphQL endpoint
await generate({
  endpoint: 'https://api.example.com/graphql',
  output: './generated',
  headers: { Authorization: 'Bearer <token>' },
  reactQuery: true,
});

// Generate ORM client from a GraphQL endpoint
await generate({
  endpoint: 'https://api.example.com/graphql',
  output: './generated',
  headers: { Authorization: 'Bearer <token>' },
  orm: true,
});

// Generate both React Query hooks and ORM client
await generate({
  endpoint: 'https://api.example.com/graphql',
  output: './generated',
  reactQuery: true,
  orm: true,
});

Generate from Database

Connect directly to a PostgreSQL database to generate code:

import { generate } from '@constructive-io/graphql-codegen';

// Generate from database with explicit schemas
await generate({
  db: {
    schemas: ['public', 'app_public'],
  },
  output: './generated',
  reactQuery: true,
});

// Generate from database using API names for automatic schema discovery
await generate({
  db: {
    apiNames: ['my_api'],
  },
  output: './generated',
  orm: true,
});

// Generate with explicit database config (overrides environment variables)
await generate({
  db: {
    config: {
      host: 'localhost',
      port: 5432,
      database: 'mydb',
      user: 'postgres',
    },
    schemas: ['public'],
  },
  output: './generated',
  reactQuery: true,
});

Generate from PGPM Module

Generate code from a PGPM module path:

import { generate } from '@constructive-io/graphql-codegen';

// Generate from a PGPM module directory
await generate({
  db: {
    pgpm: { modulePath: './packages/my-module' },
    schemas: ['public'],
  },
  output: './generated',
  reactQuery: true,
});

// Generate from a PGPM workspace with module name
await generate({
  db: {
    pgpm: {
      workspacePath: '/path/to/workspace',
      moduleName: 'my-module',
    },
    schemas: ['public'],
  },
  output: './generated',
  orm: true,
});

// Keep the ephemeral database after generation (for debugging)
await generate({
  db: {
    pgpm: { modulePath: './packages/my-module' },
    schemas: ['public'],
    keepDb: true,
  },
  output: './generated',
  reactQuery: true,
});

Configuration Options

The generate function accepts a configuration object with the following options:

interface GraphQLSDKConfigTarget {
  // Source (choose one)
  endpoint?: string;                    // GraphQL endpoint URL
  schemaFile?: string;                  // Path to GraphQL schema file (.graphql)
  db?: DbConfig;                        // Database configuration (see below)

  // Output
  output?: string;                      // Output directory (default: './generated/graphql')

  // Authentication
  headers?: Record<string, string>;     // HTTP headers for endpoint requests

  // Generator flags
  reactQuery?: boolean;                 // Generate React Query hooks (output: {output}/hooks)
  orm?: boolean;                        // Generate ORM client (output: {output}/orm)

  // Table filtering (for CRUD operations from _meta)
  tables?: {
    include?: string[];                 // Glob patterns (default: ['*'])
    exclude?: string[];                 // Glob patterns (default: [])
  };

  // Query filtering (for ALL queries from __schema)
  queries?: {
    include?: string[];                 // Glob patterns (default: ['*'])
    exclude?: string[];                 // Glob patterns (default: ['_meta', 'query'])
  };

  // Mutation filtering (for ALL mutations from __schema)
  mutations?: {
    include?: string[];                 // Glob patterns (default: ['*'])
    exclude?: string[];                 // Glob patterns (default: [])
  };

  // Code generation options
  codegen?: {
    maxFieldDepth?: number;             // Max depth for nested fields (default: 2)
    skipQueryField?: boolean;           // Skip 'query' field (default: true)
  };

  // Query key generation
  queryKeys?: {
    generateScopedKeys?: boolean;       // Generate scope-aware keys (default: true)
    generateMutationKeys?: boolean;     // Generate mutation keys (default: true)
    generateCascadeHelpers?: boolean;   // Generate invalidation helpers (default: true)
    relationships?: Record<string, { parent: string; foreignKey: string }>;
  };
}

// Database configuration for direct database introspection or PGPM module
interface DbConfig {
  // PostgreSQL connection config (falls back to PGHOST, PGPORT, etc. env vars)
  config?: Partial<PgConfig>;

  // PGPM module configuration for ephemeral database creation
  pgpm?: {
    modulePath?: string;                // Path to PGPM module directory
    workspacePath?: string;             // Path to PGPM workspace (with moduleName)
    moduleName?: string;                // Module name within workspace
  };

  // Schema selection (choose one)
  schemas?: string[];                   // Explicit PostgreSQL schema names
  apiNames?: string[];                  // API names for automatic schema discovery

  // Debugging
  keepDb?: boolean;                     // Keep ephemeral database after generation
}

React Query Hooks

The React Query hooks generator creates type-safe useQuery and useMutation hooks for your PostGraphile API, fully integrated with TanStack Query (React Query v5).

Generated Output Structure

generated/hooks/
├── index.ts              # Main barrel export (configure, hooks, types)
├── client.ts             # configure() and execute() functions
├── types.ts              # Entity interfaces, filter types, enums
├── queries/
│   ├── index.ts          # Query hooks barrel
│   ├── useCarsQuery.ts   # Table list query (findMany)
│   ├── useCarQuery.ts    # Table single item query (findOne)
│   └── ...
└── mutations/
    ├── index.ts          # Mutation hooks barrel
    ├── useCreateCarMutation.ts
    ├── useUpdateCarMutation.ts
    ├── useDeleteCarMutation.ts
    └── ...

Setup & Configuration

Configure the GraphQL client once at your app's entry point:

import { configure } from './generated/hooks';

configure({
  endpoint: 'https://api.example.com/graphql',
  headers: {
    Authorization: 'Bearer <token>',
  },
});

Table Query Hooks

For each table, two query hooks are generated:

import { useCarsQuery, useCarQuery } from './generated/hooks';

// List query with filtering, pagination, and ordering
function CarList() {
  const { data, isLoading, isError, error } = useCarsQuery({
    first: 10,
    filter: {
      brand: { equalTo: 'Tesla' },
      price: { greaterThan: 50000 },
    },
    orderBy: ['CREATED_AT_DESC'],
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data?.cars.nodes.map((car) => (
        <li key={car.id}>{car.brand} - ${car.price}</li>
      ))}
    </ul>
  );
}

// Single item query by ID
function CarDetails({ carId }: { carId: string }) {
  const { data, isLoading } = useCarQuery({ id: carId });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{data?.car?.brand}</h1>
      <p>Price: ${data?.car?.price}</p>
    </div>
  );
}

Mutation Hooks

For each table, three mutation hooks are generated:

import {
  useCreateCarMutation,
  useUpdateCarMutation,
  useDeleteCarMutation,
} from './generated/hooks';

function CarForm() {
  const createCar = useCreateCarMutation({
    onSuccess: (data) => {
      console.log('Created car:', data.createCar.car.id);
    },
  });

  return (
    <button
      onClick={() =>
        createCar.mutate({
          input: { car: { brand: 'Tesla', price: 80000 } },
        })
      }
      disabled={createCar.isPending}
    >
      Create
    </button>
  );
}

Custom Query and Mutation Hooks

Custom queries and mutations from your schema get their own hooks:

import { useCurrentUserQuery, useLoginMutation } from './generated/hooks';

function UserProfile() {
  const { data } = useCurrentUserQuery();
  return <h1>Welcome, {data?.currentUser?.username}</h1>;
}

function LoginForm() {
  const login = useLoginMutation({
    onSuccess: (data) => {
      const token = data.login.apiToken?.accessToken;
      if (token) localStorage.setItem('token', token);
    },
  });

  return (
    <button onClick={() => login.mutate({ input: { email: 'user@example.com', password: 'secret' } })}>
      Login
    </button>
  );
}

Centralized Query Keys

The codegen generates a centralized query key factory for type-safe cache management:

import { userKeys, invalidate } from './generated/hooks';
import { useQueryClient } from '@tanstack/react-query';

// Query key structure
userKeys.all;                    // ['user']
userKeys.lists();                // ['user', 'list']
userKeys.list({ first: 10 });    // ['user', 'list', { first: 10 }]
userKeys.details();              // ['user', 'detail']
userKeys.detail('user-123');     // ['user', 'detail', 'user-123']

// Invalidation helpers
const queryClient = useQueryClient();
invalidate.user.all(queryClient);
invalidate.user.lists(queryClient);
invalidate.user.detail(queryClient, userId);

ORM Client

The ORM client provides a Prisma-like fluent API for GraphQL operations without React dependencies.

Generated Output Structure

generated/orm/
├── index.ts              # createClient() factory + re-exports
├── client.ts             # OrmClient class (GraphQL executor)
├── query-builder.ts      # QueryBuilder with execute(), unwrap(), etc.
├── select-types.ts       # Type utilities for select inference
├── input-types.ts        # All generated types
├── models/
│   ├── index.ts          # Barrel export for all models
│   ├── user.ts           # UserModel class
│   └── ...
├── query/
│   └── index.ts          # Custom query operations
└── mutation/
    └── index.ts          # Custom mutation operations

Basic Usage

import { createClient } from './generated/orm';

const db = createClient({
  endpoint: 'https://api.example.com/graphql',
  headers: { Authorization: 'Bearer <token>' },
});

// Query users
const result = await db.user
  .findMany({
    select: { id: true, username: true, email: true },
    first: 20,
  })
  .execute();

if (result.ok) {
  console.log(result.data.users.nodes);
} else {
  console.error(result.errors);
}

// Create a user
const newUser = await db.user
  .create({
    data: { username: 'john', email: 'john@example.com' },
    select: { id: true, username: true },
  })
  .execute();

// Update a user
const updated = await db.user
  .update({
    where: { id: 'user-id' },
    data: { displayName: 'John Doe' },
    select: { id: true, displayName: true },
  })
  .execute();

// Delete a user
const deleted = await db.user
  .delete({ where: { id: 'user-id' } })
  .execute();

Select & Type Inference

The ORM uses const generics to infer return types based on your select clause:

const users = await db.user
  .findMany({
    select: { id: true, username: true },
  })
  .unwrap();

// TypeScript knows the exact shape:
// users.users.nodes[0] is { id: string; username: string | null }

// Accessing unselected fields is a compile error:
// users.users.nodes[0].email  // Error: Property 'email' does not exist

Relations

Relations are fully typed in Select types:

// BelongsTo relation
const orders = await db.order
  .findMany({
    select: {
      id: true,
      customer: {
        select: { id: true, username: true },
      },
    },
  })
  .unwrap();

// HasMany relation with pagination
const users = await db.user
  .findMany({
    select: {
      id: true,
      orders: {
        select: { id: true, total: true },
        first: 10,
        orderBy: ['CREATED_AT_DESC'],
      },
    },
  })
  .unwrap();

Error Handling

The ORM provides multiple ways to handle errors:

// Discriminated union (recommended)
const result = await db.user.findMany({ select: { id: true } }).execute();

if (result.ok) {
  console.log(result.data.users.nodes);
} else {
  console.error(result.errors);
}

// .unwrap() - throws on error
try {
  const data = await db.user.findMany({ select: { id: true } }).unwrap();
} catch (error) {
  if (error instanceof GraphQLRequestError) {
    console.error('GraphQL errors:', error.errors);
  }
}

// .unwrapOr() - returns default on error
const data = await db.user
  .findMany({ select: { id: true } })
  .unwrapOr({ users: { nodes: [], totalCount: 0, pageInfo: { hasNextPage: false, hasPreviousPage: false } } });

// .unwrapOrElse() - callback on error
const data = await db.user
  .findMany({ select: { id: true } })
  .unwrapOrElse((errors) => {
    console.error('Query failed:', errors);
    return { users: { nodes: [], totalCount: 0, pageInfo: { hasNextPage: false, hasPreviousPage: false } } };
  });

Custom Operations

Custom queries and mutations are available on db.query and db.mutation:

// Custom query
const currentUser = await db.query
  .currentUser({ select: { id: true, username: true } })
  .unwrap();

// Custom mutation
const login = await db.mutation
  .login(
    { input: { email: 'user@example.com', password: 'secret' } },
    { select: { apiToken: { select: { accessToken: true } } } }
  )
  .unwrap();

console.log(login.login.apiToken?.accessToken);

Configuration

Config File

Create a graphql-codegen.config.ts file:

import type { GraphQLSDKConfig } from '@constructive-io/graphql-codegen';

export default {
  endpoint: 'https://api.example.com/graphql',
  output: './generated/graphql',
  headers: {
    Authorization: 'Bearer <token>',
  },
  reactQuery: true,
  orm: true,
} satisfies GraphQLSDKConfig;

Multi-target Configuration

For multiple schema sources, export a record of named configs:

import type { GraphQLSDKMultiConfig } from '@constructive-io/graphql-codegen';

export default {
  public: {
    endpoint: 'https://api.example.com/graphql',
    output: './generated/public',
    headers: { Authorization: 'Bearer <token>' },
    reactQuery: true,
  },
  admin: {
    schemaFile: './admin.schema.graphql',
    output: './generated/admin',
    orm: true,
  },
  database: {
    db: {
      pgpm: { modulePath: './packages/my-module' },
      schemas: ['public'],
    },
    output: './generated/db',
    reactQuery: true,
    orm: true,
  },
} satisfies GraphQLSDKMultiConfig;

Run all targets with graphql-codegen or a specific target with graphql-codegen --target public.

Glob Patterns

Filter patterns support wildcards:

{
  tables: {
    include: ['User', 'Product', 'Order*'],
    exclude: ['*_archive', 'temp_*'],
  },
  queries: {
    exclude: ['_meta', 'query', '*Debug*'],
  },
  mutations: {
    include: ['create*', 'update*', 'delete*', 'login', 'register', 'logout'],
  },
}

CLI Commands

The CLI provides a convenient way to run code generation from the command line.

graphql-codegen

Generate React Query hooks and/or ORM client from various sources.

Source Options (choose one):
  -c, --config <path>              Path to config file (graphql-sdk.config.ts)
  -e, --endpoint <url>             GraphQL endpoint URL
  -s, --schema-file <path>         Path to GraphQL schema file (.graphql)
  --pgpm-module-path <path>        Path to PGPM module directory
  --pgpm-workspace-path <path>     Path to PGPM workspace (requires --pgpm-module-name)
  --pgpm-module-name <name>        PGPM module name in workspace

Database Options (for pgpm modes):
  --schemas <list>                 Comma-separated list of PostgreSQL schemas to introspect
  --api-names <list>               Comma-separated API names for automatic schema discovery
                                   (mutually exclusive with --schemas)

Generator Options:
  --react-query                    Generate React Query hooks
  --orm                            Generate ORM client
  -o, --output <dir>               Output directory
  -t, --target <name>              Target name (for multi-target configs)
  -a, --authorization <token>      Authorization header value
  --skip-custom-operations         Only generate table CRUD operations
  --dry-run                        Preview without writing files
  -v, --verbose                    Show detailed output
  --keep-db                        Keep ephemeral database after generation (pgpm modes)
  -h, --help                       Show help message
  --version                        Show version number

Examples:

# Generate React Query hooks from an endpoint
npx @constructive-io/graphql-codegen --endpoint https://api.example.com/graphql --output ./generated --react-query

# Generate ORM client from an endpoint
npx @constructive-io/graphql-codegen --endpoint https://api.example.com/graphql --output ./generated --orm

# Generate both React Query hooks and ORM client
npx @constructive-io/graphql-codegen --endpoint https://api.example.com/graphql --output ./generated --react-query --orm

# Generate from schema file
npx @constructive-io/graphql-codegen --schema-file ./schema.graphql --output ./generated --react-query

# Generate from database with schemas
npx @constructive-io/graphql-codegen --schemas public,app_public --output ./generated --react-query

# Generate from database with API names
npx @constructive-io/graphql-codegen --api-names my_api --output ./generated --orm

# Use config file
npx @constructive-io/graphql-codegen --config ./graphql-codegen.config.ts

# Generate specific target from multi-target config
npx @constructive-io/graphql-codegen --config ./graphql-codegen.config.ts --target admin

Using with Constructive CLI

The @constructive-io/cli package includes the codegen command:

# Install Constructive CLI
npm install -g @constructive-io/cli

# Run codegen via cnc
cnc codegen --endpoint http://localhost:5555/graphql --output ./codegen --react-query

Architecture

How It Works

The codegen fetches _meta for table metadata (names, fields, relations, constraints) and __schema for full schema introspection (all queries, mutations, types). It then filters operations to avoid duplicates and generates type-safe code using Babel AST.

Code Generation Pipeline

PostGraphile Endpoint
        │
        ▼
┌───────────────────┐
│  Introspection    │
│  - _meta query    │
│  - __schema       │
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  Schema Parser    │
│  - CleanTable     │
│  - CleanOperation │
│  - TypeRegistry   │
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  Code Generators  │
│  - Models         │
│  - Types          │
│  - Client         │
│  - Custom Ops     │
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  Output Files     │
│  - TypeScript     │
│  - Formatted      │
└───────────────────┘

Type Inference with Const Generics

The ORM uses TypeScript const generics to infer return types:

findMany<const S extends UserSelect>(
  args?: FindManyArgs<S, UserFilter, UsersOrderBy>
): QueryBuilder<{ users: ConnectionResult<InferSelectResult<User, S>> }>

Generated Types

Entity Types

export interface User {
  id: string;
  username?: string | null;
  displayName?: string | null;
  email?: string | null;
  createdAt?: string | null;
  updatedAt?: string | null;
}

Filter Types

export interface UserFilter {
  id?: UUIDFilter;
  username?: StringFilter;
  email?: StringFilter;
  isActive?: BooleanFilter;
  createdAt?: DatetimeFilter;
  and?: UserFilter[];
  or?: UserFilter[];
  not?: UserFilter;
}

export interface StringFilter {
  isNull?: boolean;
  equalTo?: string;
  notEqualTo?: string;
  in?: string[];
  notIn?: string[];
  contains?: string;
  startsWith?: string;
  endsWith?: string;
}

OrderBy Types

export type UsersOrderBy =
  | 'PRIMARY_KEY_ASC'
  | 'PRIMARY_KEY_DESC'
  | 'NATURAL'
  | 'ID_ASC'
  | 'ID_DESC'
  | 'USERNAME_ASC'
  | 'USERNAME_DESC'
  | 'CREATED_AT_ASC'
  | 'CREATED_AT_DESC';

Input Types

export interface CreateUserInput {
  clientMutationId?: string;
  user: {
    username?: string;
    email?: string;
    displayName?: string;
  };
}

export interface UpdateUserInput {
  clientMutationId?: string;
  id: string;
  patch: UserPatch;
}

Development

# Install dependencies
pnpm install

# Build the package
pnpm build

# Run in watch mode
pnpm dev

# Type check
pnpm lint:types

# Run tests
pnpm test

Requirements

  • Node.js >= 18
  • PostGraphile endpoint with _meta query enabled
  • React Query v5 (peer dependency for React Query hooks)
  • No dependencies for ORM client (uses native fetch)

License

MIT


Education and Tutorials

  1. 🚀 Quickstart: Getting Up and Running Get started with modular databases in minutes. Install prerequisites and deploy your first module.

  2. 📦 Modular PostgreSQL Development with Database Packages Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.

  3. ✏️ Authoring Database Changes Master the workflow for adding, organizing, and managing database changes with pgpm.

  4. 🧪 End-to-End PostgreSQL Testing with TypeScript Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.

  5. Supabase Testing Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.

  6. 💧 Drizzle ORM Testing Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.

  7. 🔧 Troubleshooting Common issues and solutions for pgpm, PostgreSQL, and testing.

📦 Package Management

  • pgpm: 🖥️ PostgreSQL Package Manager for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.

🧪 Testing

  • pgsql-test: 📊 Isolated testing environments with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
  • pgsql-seed: 🌱 PostgreSQL seeding utilities for CSV, JSON, SQL data loading, and pgpm deployment.
  • supabase-test: 🧪 Supabase-native test harness preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
  • graphile-test: 🔐 Authentication mocking for Graphile-focused test helpers and emulating row-level security contexts.
  • pg-query-context: 🔒 Session context injection to add session-local context (e.g., SET LOCAL) into queries—ideal for setting role, jwt.claims, and other session settings.

🧠 Parsing & AST

  • pgsql-parser: 🔄 SQL conversion engine that interprets and converts PostgreSQL syntax.
  • libpg-query-node: 🌉 Node.js bindings for libpg_query, converting SQL into parse trees.
  • pg-proto-parser: 📦 Protobuf parser for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
  • @pgsql/enums: 🏷️ TypeScript enums for PostgreSQL AST for safe and ergonomic parsing logic.
  • @pgsql/types: 📝 Type definitions for PostgreSQL AST nodes in TypeScript.
  • @pgsql/utils: 🛠️ AST utilities for constructing and transforming PostgreSQL syntax trees.

Credits

🛠 Built by the Constructive team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on GitHub.

Disclaimer

AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.

No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.