diff --git a/graphql/codegen/README.md b/graphql/codegen/README.md
index 8f58f0e2a..95f5d54d1 100644
--- a/graphql/codegen/README.md
+++ b/graphql/codegen/README.md
@@ -1,4 +1,16 @@
-# @constructive-io/graphql-sdk
+# @constructive-io/graphql-codegen
+
+
+
+
+
+
+
+
+
+
+
+
CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe React Query hooks or a Prisma-like ORM client from your GraphQL schema.
@@ -39,7 +51,7 @@ CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe R
## Installation
```bash
-pnpm add @constructive-io/graphql-sdk
+pnpm add @constructive-io/graphql-codegen
```
## Quick Start
@@ -53,7 +65,7 @@ npx graphql-sdk init
Creates a `graphql-sdk.config.ts` file:
```typescript
-import { defineConfig } from '@constructive-io/graphql-sdk';
+import { defineConfig } from '@constructive-io/graphql-codegen';
export default defineConfig({
endpoint: 'https://api.example.com/graphql',
diff --git a/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts b/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts
new file mode 100644
index 000000000..b36741fb1
--- /dev/null
+++ b/graphql/codegen/src/__tests__/codegen/react-query-optional.test.ts
@@ -0,0 +1,335 @@
+/**
+ * Tests for React Query optional flag in code generators
+ *
+ * Verifies that when reactQueryEnabled is false:
+ * - Query generators skip React Query imports and hooks
+ * - Mutation generators return null (since they require React Query)
+ * - Standalone fetch functions are still generated for queries
+ */
+import { generateListQueryHook, generateSingleQueryHook, generateAllQueryHooks } from '../../cli/codegen/queries';
+import { generateCreateMutationHook, generateUpdateMutationHook, generateDeleteMutationHook, generateAllMutationHooks } from '../../cli/codegen/mutations';
+import { generateCustomQueryHook, generateAllCustomQueryHooks } from '../../cli/codegen/custom-queries';
+import { generateCustomMutationHook, generateAllCustomMutationHooks } from '../../cli/codegen/custom-mutations';
+import type { CleanTable, CleanFieldType, CleanRelations, CleanOperation, CleanTypeRef, TypeRegistry } from '../../types/schema';
+
+// ============================================================================
+// Test Fixtures
+// ============================================================================
+
+const fieldTypes = {
+ uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType,
+ string: { gqlType: 'String', isArray: false } as CleanFieldType,
+ int: { gqlType: 'Int', isArray: false } as CleanFieldType,
+ datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType,
+};
+
+const emptyRelations: CleanRelations = {
+ belongsTo: [],
+ hasOne: [],
+ hasMany: [],
+ manyToMany: [],
+};
+
+function createTable(partial: Partial & { name: string }): CleanTable {
+ return {
+ name: partial.name,
+ fields: partial.fields ?? [],
+ relations: partial.relations ?? emptyRelations,
+ query: partial.query,
+ inflection: partial.inflection,
+ constraints: partial.constraints,
+ };
+}
+
+const userTable = createTable({
+ name: 'User',
+ fields: [
+ { name: 'id', type: fieldTypes.uuid },
+ { name: 'email', type: fieldTypes.string },
+ { name: 'name', type: fieldTypes.string },
+ { name: 'createdAt', type: fieldTypes.datetime },
+ ],
+ query: {
+ all: 'users',
+ one: 'user',
+ create: 'createUser',
+ update: 'updateUser',
+ delete: 'deleteUser',
+ },
+});
+
+function createTypeRef(kind: CleanTypeRef['kind'], name: string | null, ofType?: CleanTypeRef): CleanTypeRef {
+ return { kind, name, ofType };
+}
+
+const sampleQueryOperation: CleanOperation = {
+ name: 'currentUser',
+ kind: 'query',
+ args: [],
+ returnType: createTypeRef('OBJECT', 'User'),
+ description: 'Get the current authenticated user',
+};
+
+const sampleMutationOperation: CleanOperation = {
+ name: 'login',
+ kind: 'mutation',
+ args: [
+ { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) },
+ { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) },
+ ],
+ returnType: createTypeRef('OBJECT', 'LoginPayload'),
+ description: 'Authenticate user',
+};
+
+const emptyTypeRegistry: TypeRegistry = new Map();
+
+// ============================================================================
+// Tests - Query Generators with reactQueryEnabled: false
+// ============================================================================
+
+describe('Query generators with reactQueryEnabled: false', () => {
+ describe('generateListQueryHook', () => {
+ it('should not include React Query imports when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).not.toContain('@tanstack/react-query');
+ expect(result.content).not.toContain('useQuery');
+ expect(result.content).not.toContain('UseQueryOptions');
+ });
+
+ it('should not include useXxxQuery hook when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).not.toContain('export function useUsersQuery');
+ });
+
+ it('should not include prefetch function when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).not.toContain('export async function prefetchUsersQuery');
+ });
+
+ it('should still include standalone fetch function when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).toContain('export async function fetchUsersQuery');
+ });
+
+ it('should still include GraphQL document when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).toContain('usersQueryDocument');
+ });
+
+ it('should still include query key factory when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).toContain('usersQueryKey');
+ });
+
+ it('should update file header when disabled', () => {
+ const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).toContain('List query functions for User');
+ });
+ });
+
+ describe('generateSingleQueryHook', () => {
+ it('should not include React Query imports when disabled', () => {
+ const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).not.toContain('@tanstack/react-query');
+ expect(result.content).not.toContain('useQuery');
+ });
+
+ it('should not include useXxxQuery hook when disabled', () => {
+ const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).not.toContain('export function useUserQuery');
+ });
+
+ it('should still include standalone fetch function when disabled', () => {
+ const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
+ expect(result.content).toContain('export async function fetchUserQuery');
+ });
+ });
+
+ describe('generateAllQueryHooks', () => {
+ it('should generate files without React Query when disabled', () => {
+ const results = generateAllQueryHooks([userTable], { reactQueryEnabled: false });
+ expect(results.length).toBe(2); // list + single
+ for (const result of results) {
+ expect(result.content).not.toContain('@tanstack/react-query');
+ expect(result.content).not.toContain('useQuery');
+ }
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Query Generators with reactQueryEnabled: true (default)
+// ============================================================================
+
+describe('Query generators with reactQueryEnabled: true (default)', () => {
+ describe('generateListQueryHook', () => {
+ it('should include React Query imports by default', () => {
+ const result = generateListQueryHook(userTable);
+ expect(result.content).toContain('@tanstack/react-query');
+ expect(result.content).toContain('useQuery');
+ });
+
+ it('should include useXxxQuery hook by default', () => {
+ const result = generateListQueryHook(userTable);
+ expect(result.content).toContain('export function useUsersQuery');
+ });
+
+ it('should include prefetch function by default', () => {
+ const result = generateListQueryHook(userTable);
+ expect(result.content).toContain('export async function prefetchUsersQuery');
+ });
+
+ it('should include standalone fetch function by default', () => {
+ const result = generateListQueryHook(userTable);
+ expect(result.content).toContain('export async function fetchUsersQuery');
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Mutation Generators with reactQueryEnabled: false
+// ============================================================================
+
+describe('Mutation generators with reactQueryEnabled: false', () => {
+ describe('generateCreateMutationHook', () => {
+ it('should return null when disabled', () => {
+ const result = generateCreateMutationHook(userTable, { reactQueryEnabled: false });
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('generateUpdateMutationHook', () => {
+ it('should return null when disabled', () => {
+ const result = generateUpdateMutationHook(userTable, { reactQueryEnabled: false });
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('generateDeleteMutationHook', () => {
+ it('should return null when disabled', () => {
+ const result = generateDeleteMutationHook(userTable, { reactQueryEnabled: false });
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('generateAllMutationHooks', () => {
+ it('should return empty array when disabled', () => {
+ const results = generateAllMutationHooks([userTable], { reactQueryEnabled: false });
+ expect(results).toEqual([]);
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Mutation Generators with reactQueryEnabled: true (default)
+// ============================================================================
+
+describe('Mutation generators with reactQueryEnabled: true (default)', () => {
+ describe('generateCreateMutationHook', () => {
+ it('should return mutation file by default', () => {
+ const result = generateCreateMutationHook(userTable);
+ expect(result).not.toBeNull();
+ expect(result!.content).toContain('useMutation');
+ });
+ });
+
+ describe('generateAllMutationHooks', () => {
+ it('should return mutation files by default', () => {
+ const results = generateAllMutationHooks([userTable]);
+ expect(results.length).toBeGreaterThan(0);
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Custom Query Generators with reactQueryEnabled: false
+// ============================================================================
+
+describe('Custom query generators with reactQueryEnabled: false', () => {
+ describe('generateCustomQueryHook', () => {
+ it('should not include React Query imports when disabled', () => {
+ const result = generateCustomQueryHook({
+ operation: sampleQueryOperation,
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(result.content).not.toContain('@tanstack/react-query');
+ expect(result.content).not.toContain('useQuery');
+ });
+
+ it('should not include useXxxQuery hook when disabled', () => {
+ const result = generateCustomQueryHook({
+ operation: sampleQueryOperation,
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(result.content).not.toContain('export function useCurrentUserQuery');
+ });
+
+ it('should still include standalone fetch function when disabled', () => {
+ const result = generateCustomQueryHook({
+ operation: sampleQueryOperation,
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(result.content).toContain('export async function fetchCurrentUserQuery');
+ });
+ });
+
+ describe('generateAllCustomQueryHooks', () => {
+ it('should generate files without React Query when disabled', () => {
+ const results = generateAllCustomQueryHooks({
+ operations: [sampleQueryOperation],
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(results.length).toBe(1);
+ expect(results[0].content).not.toContain('@tanstack/react-query');
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Custom Mutation Generators with reactQueryEnabled: false
+// ============================================================================
+
+describe('Custom mutation generators with reactQueryEnabled: false', () => {
+ describe('generateCustomMutationHook', () => {
+ it('should return null when disabled', () => {
+ const result = generateCustomMutationHook({
+ operation: sampleMutationOperation,
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('generateAllCustomMutationHooks', () => {
+ it('should return empty array when disabled', () => {
+ const results = generateAllCustomMutationHooks({
+ operations: [sampleMutationOperation],
+ typeRegistry: emptyTypeRegistry,
+ reactQueryEnabled: false,
+ });
+ expect(results).toEqual([]);
+ });
+ });
+});
+
+// ============================================================================
+// Tests - Custom Mutation Generators with reactQueryEnabled: true (default)
+// ============================================================================
+
+describe('Custom mutation generators with reactQueryEnabled: true (default)', () => {
+ describe('generateCustomMutationHook', () => {
+ it('should return mutation file by default', () => {
+ const result = generateCustomMutationHook({
+ operation: sampleMutationOperation,
+ typeRegistry: emptyTypeRegistry,
+ });
+ expect(result).not.toBeNull();
+ expect(result!.content).toContain('useMutation');
+ });
+ });
+});
diff --git a/graphql/codegen/src/cli/codegen/custom-mutations.ts b/graphql/codegen/src/cli/codegen/custom-mutations.ts
index 73c3bd868..5deb921f7 100644
--- a/graphql/codegen/src/cli/codegen/custom-mutations.ts
+++ b/graphql/codegen/src/cli/codegen/custom-mutations.ts
@@ -51,15 +51,23 @@ export interface GenerateCustomMutationHookOptions {
typeRegistry: TypeRegistry;
maxDepth?: number;
skipQueryField?: boolean;
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
}
/**
* Generate a custom mutation hook file
+ * When reactQueryEnabled is false, returns null since mutations require React Query
*/
export function generateCustomMutationHook(
options: GenerateCustomMutationHookOptions
-): GeneratedCustomMutationFile {
- const { operation } = options;
+): GeneratedCustomMutationFile | null {
+ const { operation, reactQueryEnabled = true } = options;
+
+ // Mutations require React Query - skip generation when disabled
+ if (!reactQueryEnabled) {
+ return null;
+ }
try {
return generateCustomMutationHookInternal(options);
@@ -229,15 +237,18 @@ export interface GenerateAllCustomMutationHooksOptions {
typeRegistry: TypeRegistry;
maxDepth?: number;
skipQueryField?: boolean;
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
}
/**
* Generate all custom mutation hook files
+ * When reactQueryEnabled is false, returns empty array since mutations require React Query
*/
export function generateAllCustomMutationHooks(
options: GenerateAllCustomMutationHooksOptions
): GeneratedCustomMutationFile[] {
- const { operations, typeRegistry, maxDepth = 2, skipQueryField = true } = options;
+ const { operations, typeRegistry, maxDepth = 2, skipQueryField = true, reactQueryEnabled = true } = options;
return operations
.filter((op) => op.kind === 'mutation')
@@ -247,6 +258,8 @@ export function generateAllCustomMutationHooks(
typeRegistry,
maxDepth,
skipQueryField,
+ reactQueryEnabled,
})
- );
+ )
+ .filter((result): result is GeneratedCustomMutationFile => result !== null);
}
diff --git a/graphql/codegen/src/cli/codegen/custom-queries.ts b/graphql/codegen/src/cli/codegen/custom-queries.ts
index 567dd5d0f..05f707f19 100644
--- a/graphql/codegen/src/cli/codegen/custom-queries.ts
+++ b/graphql/codegen/src/cli/codegen/custom-queries.ts
@@ -53,6 +53,8 @@ export interface GenerateCustomQueryHookOptions {
typeRegistry: TypeRegistry;
maxDepth?: number;
skipQueryField?: boolean;
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
}
/**
@@ -61,7 +63,7 @@ export interface GenerateCustomQueryHookOptions {
export function generateCustomQueryHook(
options: GenerateCustomQueryHookOptions
): GeneratedCustomQueryFile {
- const { operation, typeRegistry, maxDepth = 2, skipQueryField = true } = options;
+ const { operation, typeRegistry, maxDepth = 2, skipQueryField = true, reactQueryEnabled = true } = options;
const project = createProject();
const hookName = getOperationHookName(operation.name, 'query');
@@ -82,24 +84,30 @@ export function generateCustomQueryHook(
const sourceFile = createSourceFile(project, fileName);
// Add file header
- sourceFile.insertText(
- 0,
- createFileHeader(`Custom query hook for ${operation.name}`) + '\n\n'
- );
-
- // Add imports
- sourceFile.addImportDeclarations([
- createImport({
- moduleSpecifier: '@tanstack/react-query',
- namedImports: ['useQuery'],
- typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
- }),
+ const headerText = reactQueryEnabled
+ ? `Custom query hook for ${operation.name}`
+ : `Custom query functions for ${operation.name}`;
+ sourceFile.insertText(0, createFileHeader(headerText) + '\n\n');
+
+ // Add imports - conditionally include React Query imports
+ const imports = [];
+ if (reactQueryEnabled) {
+ imports.push(
+ createImport({
+ moduleSpecifier: '@tanstack/react-query',
+ namedImports: ['useQuery'],
+ typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
+ })
+ );
+ }
+ imports.push(
createImport({
moduleSpecifier: '../client',
namedImports: ['execute'],
typeOnlyNamedImports: ['ExecuteOptions'],
- }),
- ]);
+ })
+ );
+ sourceFile.addImportDeclarations(imports);
// Add query document constant
sourceFile.addVariableStatement(
@@ -139,18 +147,20 @@ export function generateCustomQueryHook(
);
}
- // Generate hook function
- const hookParams = generateHookParameters(operation, variablesTypeName, resultTypeName);
- const hookBody = generateHookBody(operation, documentConstName, queryKeyName, variablesTypeName, resultTypeName);
- const hookDoc = generateHookDoc(operation, hookName);
-
- sourceFile.addFunction({
- name: hookName,
- isExported: true,
- parameters: hookParams,
- statements: hookBody,
- docs: [{ description: hookDoc }],
- });
+ // Generate hook function (only if React Query is enabled)
+ if (reactQueryEnabled) {
+ const hookParams = generateHookParameters(operation, variablesTypeName, resultTypeName);
+ const hookBody = generateHookBody(operation, documentConstName, queryKeyName, variablesTypeName, resultTypeName);
+ const hookDoc = generateHookDoc(operation, hookName);
+
+ sourceFile.addFunction({
+ name: hookName,
+ isExported: true,
+ parameters: hookParams,
+ statements: hookBody,
+ docs: [{ description: hookDoc }],
+ });
+ }
// Add standalone functions section
sourceFile.addStatements('\n// ============================================================================');
@@ -173,21 +183,23 @@ export function generateCustomQueryHook(
docs: [{ description: fetchDoc }],
});
- // Generate prefetch function
- const prefetchFnName = `prefetch${ucFirst(operation.name)}Query`;
- const prefetchParams = generatePrefetchParameters(operation, variablesTypeName);
- const prefetchBody = generatePrefetchBody(operation, documentConstName, queryKeyName, variablesTypeName, resultTypeName);
- const prefetchDoc = generatePrefetchDoc(operation, prefetchFnName);
-
- sourceFile.addFunction({
- name: prefetchFnName,
- isExported: true,
- isAsync: true,
- parameters: prefetchParams,
- returnType: 'Promise',
- statements: prefetchBody,
- docs: [{ description: prefetchDoc }],
- });
+ // Generate prefetch function (only if React Query is enabled)
+ if (reactQueryEnabled) {
+ const prefetchFnName = `prefetch${ucFirst(operation.name)}Query`;
+ const prefetchParams = generatePrefetchParameters(operation, variablesTypeName);
+ const prefetchBody = generatePrefetchBody(operation, documentConstName, queryKeyName, variablesTypeName, resultTypeName);
+ const prefetchDoc = generatePrefetchDoc(operation, prefetchFnName);
+
+ sourceFile.addFunction({
+ name: prefetchFnName,
+ isExported: true,
+ isAsync: true,
+ parameters: prefetchParams,
+ returnType: 'Promise',
+ statements: prefetchBody,
+ docs: [{ description: prefetchDoc }],
+ });
+ }
return {
fileName,
@@ -482,6 +494,8 @@ export interface GenerateAllCustomQueryHooksOptions {
typeRegistry: TypeRegistry;
maxDepth?: number;
skipQueryField?: boolean;
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
}
/**
@@ -490,7 +504,7 @@ export interface GenerateAllCustomQueryHooksOptions {
export function generateAllCustomQueryHooks(
options: GenerateAllCustomQueryHooksOptions
): GeneratedCustomQueryFile[] {
- const { operations, typeRegistry, maxDepth = 2, skipQueryField = true } = options;
+ const { operations, typeRegistry, maxDepth = 2, skipQueryField = true, reactQueryEnabled = true } = options;
return operations
.filter((op) => op.kind === 'query')
@@ -500,6 +514,7 @@ export function generateAllCustomQueryHooks(
typeRegistry,
maxDepth,
skipQueryField,
+ reactQueryEnabled,
})
);
}
diff --git a/graphql/codegen/src/cli/codegen/index.ts b/graphql/codegen/src/cli/codegen/index.ts
index fbcd8d96b..5a91b16dd 100644
--- a/graphql/codegen/src/cli/codegen/index.ts
+++ b/graphql/codegen/src/cli/codegen/index.ts
@@ -100,6 +100,7 @@ export function generate(options: GenerateOptions): GenerateResult {
// Extract codegen options
const maxDepth = config.codegen.maxFieldDepth;
const skipQueryField = config.codegen.skipQueryField;
+ const reactQueryEnabled = config.reactQuery.enabled;
// 1. Generate client.ts
files.push({
@@ -114,7 +115,7 @@ export function generate(options: GenerateOptions): GenerateResult {
});
// 3. Generate table-based query hooks (queries/*.ts)
- const queryHooks = generateAllQueryHooks(tables);
+ const queryHooks = generateAllQueryHooks(tables, { reactQueryEnabled });
for (const hook of queryHooks) {
files.push({
path: `queries/${hook.fileName}`,
@@ -130,6 +131,7 @@ export function generate(options: GenerateOptions): GenerateResult {
typeRegistry: customOperations.typeRegistry,
maxDepth,
skipQueryField,
+ reactQueryEnabled,
});
for (const hook of customQueryHooks) {
@@ -149,7 +151,7 @@ export function generate(options: GenerateOptions): GenerateResult {
});
// 6. Generate table-based mutation hooks (mutations/*.ts)
- const mutationHooks = generateAllMutationHooks(tables);
+ const mutationHooks = generateAllMutationHooks(tables, { reactQueryEnabled });
for (const hook of mutationHooks) {
files.push({
path: `mutations/${hook.fileName}`,
@@ -165,6 +167,7 @@ export function generate(options: GenerateOptions): GenerateResult {
typeRegistry: customOperations.typeRegistry,
maxDepth,
skipQueryField,
+ reactQueryEnabled,
});
for (const hook of customMutationHooks) {
diff --git a/graphql/codegen/src/cli/codegen/mutations.ts b/graphql/codegen/src/cli/codegen/mutations.ts
index 24090e855..9da44a62c 100644
--- a/graphql/codegen/src/cli/codegen/mutations.ts
+++ b/graphql/codegen/src/cli/codegen/mutations.ts
@@ -46,14 +46,29 @@ export interface GeneratedMutationFile {
content: string;
}
+export interface MutationGeneratorOptions {
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
+}
+
// ============================================================================
// Create mutation hook generator
// ============================================================================
/**
* Generate create mutation hook file content using AST
+ * When reactQueryEnabled is false, returns null since mutations require React Query
*/
-export function generateCreateMutationHook(table: CleanTable): GeneratedMutationFile {
+export function generateCreateMutationHook(
+ table: CleanTable,
+ options: MutationGeneratorOptions = {}
+): GeneratedMutationFile | null {
+ const { reactQueryEnabled = true } = options;
+
+ // Mutations require React Query - skip generation when disabled
+ if (!reactQueryEnabled) {
+ return null;
+ }
const project = createProject();
const { typeName, singularName } = getTableNames(table);
const hookName = getCreateMutationHookName(table);
@@ -208,8 +223,19 @@ mutate({
/**
* Generate update mutation hook file content using AST
+ * When reactQueryEnabled is false, returns null since mutations require React Query
*/
-export function generateUpdateMutationHook(table: CleanTable): GeneratedMutationFile | null {
+export function generateUpdateMutationHook(
+ table: CleanTable,
+ options: MutationGeneratorOptions = {}
+): GeneratedMutationFile | null {
+ const { reactQueryEnabled = true } = options;
+
+ // Mutations require React Query - skip generation when disabled
+ if (!reactQueryEnabled) {
+ return null;
+ }
+
// Check if update mutation exists
if (table.query?.update === null) {
return null;
@@ -369,8 +395,19 @@ mutate({
/**
* Generate delete mutation hook file content using AST
+ * When reactQueryEnabled is false, returns null since mutations require React Query
*/
-export function generateDeleteMutationHook(table: CleanTable): GeneratedMutationFile | null {
+export function generateDeleteMutationHook(
+ table: CleanTable,
+ options: MutationGeneratorOptions = {}
+): GeneratedMutationFile | null {
+ const { reactQueryEnabled = true } = options;
+
+ // Mutations require React Query - skip generation when disabled
+ if (!reactQueryEnabled) {
+ return null;
+ }
+
// Check if delete mutation exists
if (table.query?.delete === null) {
return null;
@@ -504,19 +541,26 @@ mutate({
/**
* Generate all mutation hook files for all tables
+ * When reactQueryEnabled is false, returns empty array since mutations require React Query
*/
-export function generateAllMutationHooks(tables: CleanTable[]): GeneratedMutationFile[] {
+export function generateAllMutationHooks(
+ tables: CleanTable[],
+ options: MutationGeneratorOptions = {}
+): GeneratedMutationFile[] {
const files: GeneratedMutationFile[] = [];
for (const table of tables) {
- files.push(generateCreateMutationHook(table));
+ const createHook = generateCreateMutationHook(table, options);
+ if (createHook) {
+ files.push(createHook);
+ }
- const updateHook = generateUpdateMutationHook(table);
+ const updateHook = generateUpdateMutationHook(table, options);
if (updateHook) {
files.push(updateHook);
}
- const deleteHook = generateDeleteMutationHook(table);
+ const deleteHook = generateDeleteMutationHook(table, options);
if (deleteHook) {
files.push(deleteHook);
}
diff --git a/graphql/codegen/src/cli/codegen/queries.ts b/graphql/codegen/src/cli/codegen/queries.ts
index 7eb30cfcb..5d3634a15 100644
--- a/graphql/codegen/src/cli/codegen/queries.ts
+++ b/graphql/codegen/src/cli/codegen/queries.ts
@@ -46,6 +46,11 @@ export interface GeneratedQueryFile {
content: string;
}
+export interface QueryGeneratorOptions {
+ /** Whether to generate React Query hooks (default: true for backwards compatibility) */
+ reactQueryEnabled?: boolean;
+}
+
// ============================================================================
// List query hook generator
// ============================================================================
@@ -53,7 +58,11 @@ export interface GeneratedQueryFile {
/**
* Generate list query hook file content using AST
*/
-export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
+export function generateListQueryHook(
+ table: CleanTable,
+ options: QueryGeneratorOptions = {}
+): GeneratedQueryFile {
+ const { reactQueryEnabled = true } = options;
const project = createProject();
const { typeName, pluralName } = getTableNames(table);
const hookName = getListQueryHookName(table);
@@ -69,7 +78,10 @@ export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
const sourceFile = createSourceFile(project, getListQueryFileName(table));
// Add file header as leading comment
- sourceFile.insertText(0, createFileHeader(`List query hook for ${typeName}`) + '\n\n');
+ const headerText = reactQueryEnabled
+ ? `List query hook for ${typeName}`
+ : `List query functions for ${typeName}`;
+ sourceFile.insertText(0, createFileHeader(headerText) + '\n\n');
// Collect all filter types used by this table's fields
const filterTypesUsed = new Set();
@@ -80,13 +92,18 @@ export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
}
}
- // Add imports
- sourceFile.addImportDeclarations([
- createImport({
- moduleSpecifier: '@tanstack/react-query',
- namedImports: ['useQuery'],
- typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
- }),
+ // Add imports - conditionally include React Query imports
+ const imports = [];
+ if (reactQueryEnabled) {
+ imports.push(
+ createImport({
+ moduleSpecifier: '@tanstack/react-query',
+ namedImports: ['useQuery'],
+ typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
+ })
+ );
+ }
+ imports.push(
createImport({
moduleSpecifier: '../client',
namedImports: ['execute'],
@@ -95,8 +112,9 @@ export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
createImport({
moduleSpecifier: '../types',
typeOnlyNamedImports: [typeName, ...Array.from(filterTypesUsed)],
- }),
- ]);
+ })
+ );
+ sourceFile.addImportDeclarations(imports);
// Re-export entity type
sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
@@ -185,28 +203,29 @@ export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
)
);
- // Add section comment
- sourceFile.addStatements('\n// ============================================================================');
- sourceFile.addStatements('// Hook');
- sourceFile.addStatements('// ============================================================================\n');
-
- // Hook function
- sourceFile.addFunction({
- name: hookName,
- isExported: true,
- parameters: [
- {
- name: 'variables',
- type: `${ucFirst(pluralName)}QueryVariables`,
- hasQuestionToken: true,
- },
- {
- name: 'options',
- type: `Omit, 'queryKey' | 'queryFn'>`,
- hasQuestionToken: true,
- },
- ],
- statements: `return useQuery({
+ // Add React Query hook section (only if enabled)
+ if (reactQueryEnabled) {
+ sourceFile.addStatements('\n// ============================================================================');
+ sourceFile.addStatements('// Hook');
+ sourceFile.addStatements('// ============================================================================\n');
+
+ // Hook function
+ sourceFile.addFunction({
+ name: hookName,
+ isExported: true,
+ parameters: [
+ {
+ name: 'variables',
+ type: `${ucFirst(pluralName)}QueryVariables`,
+ hasQuestionToken: true,
+ },
+ {
+ name: 'options',
+ type: `Omit, 'queryKey' | 'queryFn'>`,
+ hasQuestionToken: true,
+ },
+ ],
+ statements: `return useQuery({
queryKey: ${queryName}QueryKey(variables),
queryFn: () => execute<${ucFirst(pluralName)}QueryResult, ${ucFirst(pluralName)}QueryVariables>(
${queryName}QueryDocument,
@@ -214,9 +233,9 @@ export function generateListQueryHook(table: CleanTable): GeneratedQueryFile {
),
...options,
});`,
- docs: [
- {
- description: `Query hook for fetching ${typeName} list
+ docs: [
+ {
+ description: `Query hook for fetching ${typeName} list
@example
\`\`\`tsx
@@ -226,9 +245,10 @@ const { data, isLoading } = ${hookName}({
orderBy: ['CREATED_AT_DESC'],
});
\`\`\``,
- },
- ],
- });
+ },
+ ],
+ });
+ }
// Add section comment for standalone functions
sourceFile.addStatements('\n// ============================================================================');
@@ -277,29 +297,30 @@ const data = await queryClient.fetchQuery({
],
});
- // Prefetch function (for SSR/QueryClient)
- sourceFile.addFunction({
- name: `prefetch${ucFirst(pluralName)}Query`,
- isExported: true,
- isAsync: true,
- parameters: [
- {
- name: 'queryClient',
- type: 'QueryClient',
- },
- {
- name: 'variables',
- type: `${ucFirst(pluralName)}QueryVariables`,
- hasQuestionToken: true,
- },
- {
- name: 'options',
- type: 'ExecuteOptions',
- hasQuestionToken: true,
- },
- ],
- returnType: 'Promise',
- statements: `await queryClient.prefetchQuery({
+ // Prefetch function (for SSR/QueryClient) - only if React Query is enabled
+ if (reactQueryEnabled) {
+ sourceFile.addFunction({
+ name: `prefetch${ucFirst(pluralName)}Query`,
+ isExported: true,
+ isAsync: true,
+ parameters: [
+ {
+ name: 'queryClient',
+ type: 'QueryClient',
+ },
+ {
+ name: 'variables',
+ type: `${ucFirst(pluralName)}QueryVariables`,
+ hasQuestionToken: true,
+ },
+ {
+ name: 'options',
+ type: 'ExecuteOptions',
+ hasQuestionToken: true,
+ },
+ ],
+ returnType: 'Promise',
+ statements: `await queryClient.prefetchQuery({
queryKey: ${queryName}QueryKey(variables),
queryFn: () => execute<${ucFirst(pluralName)}QueryResult, ${ucFirst(pluralName)}QueryVariables>(
${queryName}QueryDocument,
@@ -307,17 +328,18 @@ const data = await queryClient.fetchQuery({
options
),
});`,
- docs: [
- {
- description: `Prefetch ${typeName} list for SSR or cache warming
+ docs: [
+ {
+ description: `Prefetch ${typeName} list for SSR or cache warming
@example
\`\`\`ts
await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });
\`\`\``,
- },
- ],
- });
+ },
+ ],
+ });
+ }
return {
fileName: getListQueryFileName(table),
@@ -332,7 +354,11 @@ await prefetch${ucFirst(pluralName)}Query(queryClient, { first: 10 });
/**
* Generate single item query hook file content using AST
*/
-export function generateSingleQueryHook(table: CleanTable): GeneratedQueryFile {
+export function generateSingleQueryHook(
+ table: CleanTable,
+ options: QueryGeneratorOptions = {}
+): GeneratedQueryFile {
+ const { reactQueryEnabled = true } = options;
const project = createProject();
const { typeName, singularName } = getTableNames(table);
const hookName = getSingleQueryHookName(table);
@@ -345,15 +371,23 @@ export function generateSingleQueryHook(table: CleanTable): GeneratedQueryFile {
const sourceFile = createSourceFile(project, getSingleQueryFileName(table));
// Add file header
- sourceFile.insertText(0, createFileHeader(`Single item query hook for ${typeName}`) + '\n\n');
-
- // Add imports
- sourceFile.addImportDeclarations([
- createImport({
- moduleSpecifier: '@tanstack/react-query',
- namedImports: ['useQuery'],
- typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
- }),
+ const headerText = reactQueryEnabled
+ ? `Single item query hook for ${typeName}`
+ : `Single item query functions for ${typeName}`;
+ sourceFile.insertText(0, createFileHeader(headerText) + '\n\n');
+
+ // Add imports - conditionally include React Query imports
+ const imports = [];
+ if (reactQueryEnabled) {
+ imports.push(
+ createImport({
+ moduleSpecifier: '@tanstack/react-query',
+ namedImports: ['useQuery'],
+ typeOnlyNamedImports: ['UseQueryOptions', 'QueryClient'],
+ })
+ );
+ }
+ imports.push(
createImport({
moduleSpecifier: '../client',
namedImports: ['execute'],
@@ -362,8 +396,9 @@ export function generateSingleQueryHook(table: CleanTable): GeneratedQueryFile {
createImport({
moduleSpecifier: '../types',
typeOnlyNamedImports: [typeName],
- }),
- ]);
+ })
+ );
+ sourceFile.addImportDeclarations(imports);
// Re-export entity type
sourceFile.addStatements(`\n// Re-export entity type for convenience\nexport type { ${typeName} };\n`);
@@ -411,24 +446,25 @@ export function generateSingleQueryHook(table: CleanTable): GeneratedQueryFile {
)
);
- // Add section comment
- sourceFile.addStatements('\n// ============================================================================');
- sourceFile.addStatements('// Hook');
- sourceFile.addStatements('// ============================================================================\n');
-
- // Hook function
- sourceFile.addFunction({
- name: hookName,
- isExported: true,
- parameters: [
- { name: 'id', type: 'string' },
- {
- name: 'options',
- type: `Omit, 'queryKey' | 'queryFn'>`,
- hasQuestionToken: true,
- },
- ],
- statements: `return useQuery({
+ // Add React Query hook section (only if enabled)
+ if (reactQueryEnabled) {
+ sourceFile.addStatements('\n// ============================================================================');
+ sourceFile.addStatements('// Hook');
+ sourceFile.addStatements('// ============================================================================\n');
+
+ // Hook function
+ sourceFile.addFunction({
+ name: hookName,
+ isExported: true,
+ parameters: [
+ { name: 'id', type: 'string' },
+ {
+ name: 'options',
+ type: `Omit, 'queryKey' | 'queryFn'>`,
+ hasQuestionToken: true,
+ },
+ ],
+ statements: `return useQuery({
queryKey: ${queryName}QueryKey(id),
queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
${queryName}QueryDocument,
@@ -437,9 +473,9 @@ export function generateSingleQueryHook(table: CleanTable): GeneratedQueryFile {
enabled: !!id && (options?.enabled !== false),
...options,
});`,
- docs: [
- {
- description: `Query hook for fetching a single ${typeName} by ID
+ docs: [
+ {
+ description: `Query hook for fetching a single ${typeName} by ID
@example
\`\`\`tsx
@@ -449,9 +485,10 @@ if (data?.${queryName}) {
console.log(data.${queryName}.id);
}
\`\`\``,
- },
- ],
- });
+ },
+ ],
+ });
+ }
// Add section comment for standalone functions
sourceFile.addStatements('\n// ============================================================================');
@@ -489,22 +526,23 @@ const data = await fetch${ucFirst(singularName)}Query('uuid-here');
],
});
- // Prefetch function (for SSR/QueryClient)
- sourceFile.addFunction({
- name: `prefetch${ucFirst(singularName)}Query`,
- isExported: true,
- isAsync: true,
- parameters: [
- { name: 'queryClient', type: 'QueryClient' },
- { name: 'id', type: 'string' },
- {
- name: 'options',
- type: 'ExecuteOptions',
- hasQuestionToken: true,
- },
- ],
- returnType: 'Promise',
- statements: `await queryClient.prefetchQuery({
+ // Prefetch function (for SSR/QueryClient) - only if React Query is enabled
+ if (reactQueryEnabled) {
+ sourceFile.addFunction({
+ name: `prefetch${ucFirst(singularName)}Query`,
+ isExported: true,
+ isAsync: true,
+ parameters: [
+ { name: 'queryClient', type: 'QueryClient' },
+ { name: 'id', type: 'string' },
+ {
+ name: 'options',
+ type: 'ExecuteOptions',
+ hasQuestionToken: true,
+ },
+ ],
+ returnType: 'Promise',
+ statements: `await queryClient.prefetchQuery({
queryKey: ${queryName}QueryKey(id),
queryFn: () => execute<${ucFirst(singularName)}QueryResult, ${ucFirst(singularName)}QueryVariables>(
${queryName}QueryDocument,
@@ -512,17 +550,18 @@ const data = await fetch${ucFirst(singularName)}Query('uuid-here');
options
),
});`,
- docs: [
- {
- description: `Prefetch a single ${typeName} for SSR or cache warming
+ docs: [
+ {
+ description: `Prefetch a single ${typeName} for SSR or cache warming
@example
\`\`\`ts
await prefetch${ucFirst(singularName)}Query(queryClient, 'uuid-here');
\`\`\``,
- },
- ],
- });
+ },
+ ],
+ });
+ }
return {
fileName: getSingleQueryFileName(table),
@@ -537,12 +576,15 @@ await prefetch${ucFirst(singularName)}Query(queryClient, 'uuid-here');
/**
* Generate all query hook files for all tables
*/
-export function generateAllQueryHooks(tables: CleanTable[]): GeneratedQueryFile[] {
+export function generateAllQueryHooks(
+ tables: CleanTable[],
+ options: QueryGeneratorOptions = {}
+): GeneratedQueryFile[] {
const files: GeneratedQueryFile[] = [];
for (const table of tables) {
- files.push(generateListQueryHook(table));
- files.push(generateSingleQueryHook(table));
+ files.push(generateListQueryHook(table, options));
+ files.push(generateSingleQueryHook(table, options));
}
return files;
diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts
index 872ac682a..5584559e6 100644
--- a/graphql/codegen/src/types/config.ts
+++ b/graphql/codegen/src/types/config.ts
@@ -107,6 +107,19 @@ export interface GraphQLSDKConfig {
useSharedTypes?: boolean;
};
+ /**
+ * React Query integration options
+ * Controls whether React Query hooks are generated
+ */
+ reactQuery?: {
+ /**
+ * Whether to generate React Query hooks (useQuery, useMutation)
+ * When false, only standalone fetch functions are generated (no React dependency)
+ * @default false
+ */
+ enabled?: boolean;
+ };
+
/**
* Watch mode configuration (dev-only feature)
* When enabled via CLI --watch flag, the CLI will poll the endpoint for schema changes
@@ -160,7 +173,7 @@ export interface ResolvedWatchConfig {
/**
* Resolved configuration with defaults applied
*/
-export interface ResolvedConfig extends Required> {
+export interface ResolvedConfig extends Required> {
headers: Record;
tables: {
include: string[];
@@ -190,6 +203,9 @@ export interface ResolvedConfig extends Required = {
skipQueryField: true,
},
orm: null, // ORM generation disabled by default
+ reactQuery: {
+ enabled: false, // React Query hooks disabled by default
+ },
watch: DEFAULT_WATCH_CONFIG,
};
@@ -292,6 +311,9 @@ export function resolveConfig(config: GraphQLSDKConfig): ResolvedConfig {
useSharedTypes: config.orm.useSharedTypes ?? DEFAULT_ORM_CONFIG.useSharedTypes,
}
: null,
+ reactQuery: {
+ enabled: config.reactQuery?.enabled ?? DEFAULT_CONFIG.reactQuery.enabled,
+ },
watch: {
pollInterval: config.watch?.pollInterval ?? DEFAULT_WATCH_CONFIG.pollInterval,
debounce: config.watch?.debounce ?? DEFAULT_WATCH_CONFIG.debounce,