---
name: graphql-module
description:
  When creating, modifying, or debugging GraphQL modules, resolvers, providers, or type definitions
  in the server package
---

# GraphQL Module Patterns

## Module Anatomy

Each module in `src/modules/<name>/` contains:

- `typeDefs/<name>.graphql.ts` — schema using `gql` tag from `graphql-modules`
- `resolvers/<name>.resolver.ts` — query, mutation, and field resolvers
- `providers/<name>.provider.ts` — data access layer with `@Injectable()`
- `helpers/` — module-specific utility functions
- `types.ts` — re-exports generated types from `__generated__/`
- `index.ts` — `createModule()` registration

See `references/` for annotated examples of each file.

## Provider Injection

- Providers use `@Injectable({ scope: Scope.Operation, global: true })` from `graphql-modules`
- Constructor injection for dependencies: `constructor(private db: TenantAwareDBClient, ...)`
- Database access via `TenantAwareDBClient` (tenant-scoped) — never raw Postgres clients
- Admin context via `AdminContextProvider` when owner/tenant ID is needed

## SQL Queries (PgTyped — mandatory)

- **Avoid inline SQL** with raw pg or tagged strings without a type parameter.
- All SQL better use `sql<IQueryType>` from `@pgtyped/runtime`, defined as module-level constants
  **outside** the class.
- The `IQueryType` interface is generated by `yarn generate` from the SQL — never hand-write it.
- Call queries via `.run(params, this.db)` inside provider methods.
- See `references/example-provider.ts` for the correct pattern.

## DataLoader for N+1 Prevention

- Every provider that fetches by ID must expose a DataLoader
  (`new DataLoader(keys => this.batchFn(keys))`)
- Batch function receives `readonly` array of keys, returns results in matching order
- Prime the loader cache when fetching all records (e.g., `getAll` primes individual loaders)
- Call `clearAll()` on loaders after mutations to invalidate cache

## Field Resolvers

- Field resolvers on extended types resolve cross-module relationships
- Access the parent's DB fields (snake_case) to load related data via providers
- Return `null` explicitly when the foreign key is missing — don't throw

## Type Generation

- Types come from `__generated__/types.ts` (graphql-codegen) and `__generated__/<name>.types.ts`
  (pgtyped)
- Module resolver type: `<ModuleName>Module.Resolvers` from `./types.js`
- Never hand-write GraphQL resolver types — always import generated ones
- Run `yarn generate` after any schema or SQL change

## Gotchas

- Always import with `.js` extension — even for `.ts` source files (ESM requirement)
- Never access DB directly from resolvers — always go through providers
- `__generated__/` is git-ignored — run `yarn generate` before working on a module for the first
  time
- Provider classes must have `@Injectable()` decorator or DI fails silently at runtime
- SQL queries use `@pgtyped/runtime`'s `sql` tagged template — types are generated from the SQL, not
  hand-written
- Auth directives (`@requiresAuth`, `@requiresAnyRole`) go on schema fields, not in resolver code
- `Scope.Operation` ensures one provider instance per GraphQL operation (request-scoped)
