Skip to content

Commit 834ae6a

Browse files
authored
Merge pull request #570 from constructive-io/devin/1767752540-react-query-optional
feat(graphql-codegen): make React Query optional with config flag
2 parents fc89c64 + 07c5ccc commit 834ae6a

8 files changed

Lines changed: 675 additions & 189 deletions

File tree

graphql/codegen/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
# @constructive-io/graphql-sdk
1+
# @constructive-io/graphql-codegen
2+
3+
<p align="center" width="100%">
4+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9+
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10+
</a>
11+
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12+
<a href="https://www.npmjs.com/package/@constructive-io/graphql-codegen"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphql%2Fcodegen%2Fpackage.json"/></a>
13+
</p>
214

315
CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe React Query hooks or a Prisma-like ORM client from your GraphQL schema.
416

@@ -39,7 +51,7 @@ CLI-based GraphQL SDK generator for PostGraphile endpoints. Generate type-safe R
3951
## Installation
4052

4153
```bash
42-
pnpm add @constructive-io/graphql-sdk
54+
pnpm add @constructive-io/graphql-codegen
4355
```
4456

4557
## Quick Start
@@ -53,7 +65,7 @@ npx graphql-sdk init
5365
Creates a `graphql-sdk.config.ts` file:
5466

5567
```typescript
56-
import { defineConfig } from '@constructive-io/graphql-sdk';
68+
import { defineConfig } from '@constructive-io/graphql-codegen';
5769

5870
export default defineConfig({
5971
endpoint: 'https://api.example.com/graphql',
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/**
2+
* Tests for React Query optional flag in code generators
3+
*
4+
* Verifies that when reactQueryEnabled is false:
5+
* - Query generators skip React Query imports and hooks
6+
* - Mutation generators return null (since they require React Query)
7+
* - Standalone fetch functions are still generated for queries
8+
*/
9+
import { generateListQueryHook, generateSingleQueryHook, generateAllQueryHooks } from '../../cli/codegen/queries';
10+
import { generateCreateMutationHook, generateUpdateMutationHook, generateDeleteMutationHook, generateAllMutationHooks } from '../../cli/codegen/mutations';
11+
import { generateCustomQueryHook, generateAllCustomQueryHooks } from '../../cli/codegen/custom-queries';
12+
import { generateCustomMutationHook, generateAllCustomMutationHooks } from '../../cli/codegen/custom-mutations';
13+
import type { CleanTable, CleanFieldType, CleanRelations, CleanOperation, CleanTypeRef, TypeRegistry } from '../../types/schema';
14+
15+
// ============================================================================
16+
// Test Fixtures
17+
// ============================================================================
18+
19+
const fieldTypes = {
20+
uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType,
21+
string: { gqlType: 'String', isArray: false } as CleanFieldType,
22+
int: { gqlType: 'Int', isArray: false } as CleanFieldType,
23+
datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType,
24+
};
25+
26+
const emptyRelations: CleanRelations = {
27+
belongsTo: [],
28+
hasOne: [],
29+
hasMany: [],
30+
manyToMany: [],
31+
};
32+
33+
function createTable(partial: Partial<CleanTable> & { name: string }): CleanTable {
34+
return {
35+
name: partial.name,
36+
fields: partial.fields ?? [],
37+
relations: partial.relations ?? emptyRelations,
38+
query: partial.query,
39+
inflection: partial.inflection,
40+
constraints: partial.constraints,
41+
};
42+
}
43+
44+
const userTable = createTable({
45+
name: 'User',
46+
fields: [
47+
{ name: 'id', type: fieldTypes.uuid },
48+
{ name: 'email', type: fieldTypes.string },
49+
{ name: 'name', type: fieldTypes.string },
50+
{ name: 'createdAt', type: fieldTypes.datetime },
51+
],
52+
query: {
53+
all: 'users',
54+
one: 'user',
55+
create: 'createUser',
56+
update: 'updateUser',
57+
delete: 'deleteUser',
58+
},
59+
});
60+
61+
function createTypeRef(kind: CleanTypeRef['kind'], name: string | null, ofType?: CleanTypeRef): CleanTypeRef {
62+
return { kind, name, ofType };
63+
}
64+
65+
const sampleQueryOperation: CleanOperation = {
66+
name: 'currentUser',
67+
kind: 'query',
68+
args: [],
69+
returnType: createTypeRef('OBJECT', 'User'),
70+
description: 'Get the current authenticated user',
71+
};
72+
73+
const sampleMutationOperation: CleanOperation = {
74+
name: 'login',
75+
kind: 'mutation',
76+
args: [
77+
{ name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) },
78+
{ name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) },
79+
],
80+
returnType: createTypeRef('OBJECT', 'LoginPayload'),
81+
description: 'Authenticate user',
82+
};
83+
84+
const emptyTypeRegistry: TypeRegistry = new Map();
85+
86+
// ============================================================================
87+
// Tests - Query Generators with reactQueryEnabled: false
88+
// ============================================================================
89+
90+
describe('Query generators with reactQueryEnabled: false', () => {
91+
describe('generateListQueryHook', () => {
92+
it('should not include React Query imports when disabled', () => {
93+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
94+
expect(result.content).not.toContain('@tanstack/react-query');
95+
expect(result.content).not.toContain('useQuery');
96+
expect(result.content).not.toContain('UseQueryOptions');
97+
});
98+
99+
it('should not include useXxxQuery hook when disabled', () => {
100+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
101+
expect(result.content).not.toContain('export function useUsersQuery');
102+
});
103+
104+
it('should not include prefetch function when disabled', () => {
105+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
106+
expect(result.content).not.toContain('export async function prefetchUsersQuery');
107+
});
108+
109+
it('should still include standalone fetch function when disabled', () => {
110+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
111+
expect(result.content).toContain('export async function fetchUsersQuery');
112+
});
113+
114+
it('should still include GraphQL document when disabled', () => {
115+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
116+
expect(result.content).toContain('usersQueryDocument');
117+
});
118+
119+
it('should still include query key factory when disabled', () => {
120+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
121+
expect(result.content).toContain('usersQueryKey');
122+
});
123+
124+
it('should update file header when disabled', () => {
125+
const result = generateListQueryHook(userTable, { reactQueryEnabled: false });
126+
expect(result.content).toContain('List query functions for User');
127+
});
128+
});
129+
130+
describe('generateSingleQueryHook', () => {
131+
it('should not include React Query imports when disabled', () => {
132+
const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
133+
expect(result.content).not.toContain('@tanstack/react-query');
134+
expect(result.content).not.toContain('useQuery');
135+
});
136+
137+
it('should not include useXxxQuery hook when disabled', () => {
138+
const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
139+
expect(result.content).not.toContain('export function useUserQuery');
140+
});
141+
142+
it('should still include standalone fetch function when disabled', () => {
143+
const result = generateSingleQueryHook(userTable, { reactQueryEnabled: false });
144+
expect(result.content).toContain('export async function fetchUserQuery');
145+
});
146+
});
147+
148+
describe('generateAllQueryHooks', () => {
149+
it('should generate files without React Query when disabled', () => {
150+
const results = generateAllQueryHooks([userTable], { reactQueryEnabled: false });
151+
expect(results.length).toBe(2); // list + single
152+
for (const result of results) {
153+
expect(result.content).not.toContain('@tanstack/react-query');
154+
expect(result.content).not.toContain('useQuery');
155+
}
156+
});
157+
});
158+
});
159+
160+
// ============================================================================
161+
// Tests - Query Generators with reactQueryEnabled: true (default)
162+
// ============================================================================
163+
164+
describe('Query generators with reactQueryEnabled: true (default)', () => {
165+
describe('generateListQueryHook', () => {
166+
it('should include React Query imports by default', () => {
167+
const result = generateListQueryHook(userTable);
168+
expect(result.content).toContain('@tanstack/react-query');
169+
expect(result.content).toContain('useQuery');
170+
});
171+
172+
it('should include useXxxQuery hook by default', () => {
173+
const result = generateListQueryHook(userTable);
174+
expect(result.content).toContain('export function useUsersQuery');
175+
});
176+
177+
it('should include prefetch function by default', () => {
178+
const result = generateListQueryHook(userTable);
179+
expect(result.content).toContain('export async function prefetchUsersQuery');
180+
});
181+
182+
it('should include standalone fetch function by default', () => {
183+
const result = generateListQueryHook(userTable);
184+
expect(result.content).toContain('export async function fetchUsersQuery');
185+
});
186+
});
187+
});
188+
189+
// ============================================================================
190+
// Tests - Mutation Generators with reactQueryEnabled: false
191+
// ============================================================================
192+
193+
describe('Mutation generators with reactQueryEnabled: false', () => {
194+
describe('generateCreateMutationHook', () => {
195+
it('should return null when disabled', () => {
196+
const result = generateCreateMutationHook(userTable, { reactQueryEnabled: false });
197+
expect(result).toBeNull();
198+
});
199+
});
200+
201+
describe('generateUpdateMutationHook', () => {
202+
it('should return null when disabled', () => {
203+
const result = generateUpdateMutationHook(userTable, { reactQueryEnabled: false });
204+
expect(result).toBeNull();
205+
});
206+
});
207+
208+
describe('generateDeleteMutationHook', () => {
209+
it('should return null when disabled', () => {
210+
const result = generateDeleteMutationHook(userTable, { reactQueryEnabled: false });
211+
expect(result).toBeNull();
212+
});
213+
});
214+
215+
describe('generateAllMutationHooks', () => {
216+
it('should return empty array when disabled', () => {
217+
const results = generateAllMutationHooks([userTable], { reactQueryEnabled: false });
218+
expect(results).toEqual([]);
219+
});
220+
});
221+
});
222+
223+
// ============================================================================
224+
// Tests - Mutation Generators with reactQueryEnabled: true (default)
225+
// ============================================================================
226+
227+
describe('Mutation generators with reactQueryEnabled: true (default)', () => {
228+
describe('generateCreateMutationHook', () => {
229+
it('should return mutation file by default', () => {
230+
const result = generateCreateMutationHook(userTable);
231+
expect(result).not.toBeNull();
232+
expect(result!.content).toContain('useMutation');
233+
});
234+
});
235+
236+
describe('generateAllMutationHooks', () => {
237+
it('should return mutation files by default', () => {
238+
const results = generateAllMutationHooks([userTable]);
239+
expect(results.length).toBeGreaterThan(0);
240+
});
241+
});
242+
});
243+
244+
// ============================================================================
245+
// Tests - Custom Query Generators with reactQueryEnabled: false
246+
// ============================================================================
247+
248+
describe('Custom query generators with reactQueryEnabled: false', () => {
249+
describe('generateCustomQueryHook', () => {
250+
it('should not include React Query imports when disabled', () => {
251+
const result = generateCustomQueryHook({
252+
operation: sampleQueryOperation,
253+
typeRegistry: emptyTypeRegistry,
254+
reactQueryEnabled: false,
255+
});
256+
expect(result.content).not.toContain('@tanstack/react-query');
257+
expect(result.content).not.toContain('useQuery');
258+
});
259+
260+
it('should not include useXxxQuery hook when disabled', () => {
261+
const result = generateCustomQueryHook({
262+
operation: sampleQueryOperation,
263+
typeRegistry: emptyTypeRegistry,
264+
reactQueryEnabled: false,
265+
});
266+
expect(result.content).not.toContain('export function useCurrentUserQuery');
267+
});
268+
269+
it('should still include standalone fetch function when disabled', () => {
270+
const result = generateCustomQueryHook({
271+
operation: sampleQueryOperation,
272+
typeRegistry: emptyTypeRegistry,
273+
reactQueryEnabled: false,
274+
});
275+
expect(result.content).toContain('export async function fetchCurrentUserQuery');
276+
});
277+
});
278+
279+
describe('generateAllCustomQueryHooks', () => {
280+
it('should generate files without React Query when disabled', () => {
281+
const results = generateAllCustomQueryHooks({
282+
operations: [sampleQueryOperation],
283+
typeRegistry: emptyTypeRegistry,
284+
reactQueryEnabled: false,
285+
});
286+
expect(results.length).toBe(1);
287+
expect(results[0].content).not.toContain('@tanstack/react-query');
288+
});
289+
});
290+
});
291+
292+
// ============================================================================
293+
// Tests - Custom Mutation Generators with reactQueryEnabled: false
294+
// ============================================================================
295+
296+
describe('Custom mutation generators with reactQueryEnabled: false', () => {
297+
describe('generateCustomMutationHook', () => {
298+
it('should return null when disabled', () => {
299+
const result = generateCustomMutationHook({
300+
operation: sampleMutationOperation,
301+
typeRegistry: emptyTypeRegistry,
302+
reactQueryEnabled: false,
303+
});
304+
expect(result).toBeNull();
305+
});
306+
});
307+
308+
describe('generateAllCustomMutationHooks', () => {
309+
it('should return empty array when disabled', () => {
310+
const results = generateAllCustomMutationHooks({
311+
operations: [sampleMutationOperation],
312+
typeRegistry: emptyTypeRegistry,
313+
reactQueryEnabled: false,
314+
});
315+
expect(results).toEqual([]);
316+
});
317+
});
318+
});
319+
320+
// ============================================================================
321+
// Tests - Custom Mutation Generators with reactQueryEnabled: true (default)
322+
// ============================================================================
323+
324+
describe('Custom mutation generators with reactQueryEnabled: true (default)', () => {
325+
describe('generateCustomMutationHook', () => {
326+
it('should return mutation file by default', () => {
327+
const result = generateCustomMutationHook({
328+
operation: sampleMutationOperation,
329+
typeRegistry: emptyTypeRegistry,
330+
});
331+
expect(result).not.toBeNull();
332+
expect(result!.content).toContain('useMutation');
333+
});
334+
});
335+
});

0 commit comments

Comments
 (0)