@constructive-io/graphql-codegen 3.0.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,322 +43,61 @@ const utils_1 = require("../utils");
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  /**
46
- * Generate the main client.ts file (OrmClient class)
47
- * This is the runtime client that handles GraphQL execution
48
- */
49
- function generateOrmClientFile() {
50
- // This is runtime code that doesn't change based on schema
51
- // We generate it as a static file
52
- const content = `/**
53
- * ORM Client - Runtime GraphQL executor
54
- * @generated by @constructive-io/graphql-codegen
55
- * DO NOT EDIT - changes will be overwritten
56
- */
57
-
58
- import type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
59
-
60
- export type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
61
-
62
- /**
63
- * Default adapter that uses fetch for HTTP requests.
64
- * This is used when no custom adapter is provided.
46
+ * Find a template file path.
47
+ * Templates are at ../templates/ relative to this file in both src/ and dist/.
65
48
  */
66
- export class FetchAdapter implements GraphQLAdapter {
67
- private headers: Record<string, string>;
68
-
69
- constructor(
70
- private endpoint: string,
71
- headers?: Record<string, string>
72
- ) {
73
- this.headers = headers ?? {};
74
- }
75
-
76
- async execute<T>(
77
- document: string,
78
- variables?: Record<string, unknown>
79
- ): Promise<QueryResult<T>> {
80
- const response = await fetch(this.endpoint, {
81
- method: 'POST',
82
- headers: {
83
- 'Content-Type': 'application/json',
84
- Accept: 'application/json',
85
- ...this.headers,
86
- },
87
- body: JSON.stringify({
88
- query: document,
89
- variables: variables ?? {},
90
- }),
91
- });
92
-
93
- if (!response.ok) {
94
- return {
95
- ok: false,
96
- data: null,
97
- errors: [{ message: \`HTTP \${response.status}: \${response.statusText}\` }],
98
- };
49
+ function findTemplateFile(templateName) {
50
+ const templatePath = path.join(__dirname, '../templates', templateName);
51
+ if (fs.existsSync(templatePath)) {
52
+ return templatePath;
99
53
  }
100
-
101
- const json = (await response.json()) as {
102
- data?: T;
103
- errors?: GraphQLError[];
104
- };
105
-
106
- if (json.errors && json.errors.length > 0) {
107
- return {
108
- ok: false,
109
- data: null,
110
- errors: json.errors,
111
- };
112
- }
113
-
114
- return {
115
- ok: true,
116
- data: json.data as T,
117
- errors: undefined,
118
- };
119
- }
120
-
121
- setHeaders(headers: Record<string, string>): void {
122
- this.headers = { ...this.headers, ...headers };
123
- }
124
-
125
- getEndpoint(): string {
126
- return this.endpoint;
127
- }
54
+ throw new Error(`Could not find template file: ${templateName}. ` +
55
+ `Searched in: ${templatePath}`);
128
56
  }
129
-
130
57
  /**
131
- * Configuration for creating an ORM client.
132
- * Either provide endpoint (and optional headers) for HTTP requests,
133
- * or provide a custom adapter for alternative execution strategies.
58
+ * Read a template file and replace the header with generated file header
134
59
  */
135
- export interface OrmClientConfig {
136
- /** GraphQL endpoint URL (required if adapter not provided) */
137
- endpoint?: string;
138
- /** Default headers for HTTP requests (only used with endpoint) */
139
- headers?: Record<string, string>;
140
- /** Custom adapter for GraphQL execution (overrides endpoint/headers) */
141
- adapter?: GraphQLAdapter;
60
+ function readTemplateFile(templateName, description) {
61
+ const templatePath = findTemplateFile(templateName);
62
+ let content = fs.readFileSync(templatePath, 'utf-8');
63
+ // Replace the source file header comment with the generated file header
64
+ // Match the header pattern used in template files
65
+ const headerPattern = /\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
66
+ content = content.replace(headerPattern, (0, utils_1.getGeneratedFileHeader)(description) + '\n');
67
+ return content;
142
68
  }
143
-
144
69
  /**
145
- * Error thrown when GraphQL request fails
70
+ * Generate the main client.ts file (OrmClient class)
71
+ * This is the runtime client that handles GraphQL execution
72
+ *
73
+ * Reads from the templates directory for proper type checking.
146
74
  */
147
- export class GraphQLRequestError extends Error {
148
- constructor(
149
- public readonly errors: GraphQLError[],
150
- public readonly data: unknown = null
151
- ) {
152
- const messages = errors.map(e => e.message).join('; ');
153
- super(\`GraphQL Error: \${messages}\`);
154
- this.name = 'GraphQLRequestError';
155
- }
156
- }
157
-
158
- export class OrmClient {
159
- private adapter: GraphQLAdapter;
160
-
161
- constructor(config: OrmClientConfig) {
162
- if (config.adapter) {
163
- this.adapter = config.adapter;
164
- } else if (config.endpoint) {
165
- this.adapter = new FetchAdapter(config.endpoint, config.headers);
166
- } else {
167
- throw new Error('OrmClientConfig requires either an endpoint or a custom adapter');
168
- }
169
- }
170
-
171
- async execute<T>(
172
- document: string,
173
- variables?: Record<string, unknown>
174
- ): Promise<QueryResult<T>> {
175
- return this.adapter.execute<T>(document, variables);
176
- }
177
-
178
- /**
179
- * Set headers for requests.
180
- * Only works if the adapter supports headers.
181
- */
182
- setHeaders(headers: Record<string, string>): void {
183
- if (this.adapter.setHeaders) {
184
- this.adapter.setHeaders(headers);
185
- }
186
- }
187
-
188
- /**
189
- * Get the endpoint URL.
190
- * Returns empty string if the adapter doesn't have an endpoint.
191
- */
192
- getEndpoint(): string {
193
- return this.adapter.getEndpoint?.() ?? '';
194
- }
195
- }
196
- `;
75
+ function generateOrmClientFile() {
197
76
  return {
198
77
  fileName: 'client.ts',
199
- content,
78
+ content: readTemplateFile('orm-client.ts', 'ORM Client - Runtime GraphQL executor'),
200
79
  };
201
80
  }
202
81
  /**
203
82
  * Generate the query-builder.ts file (runtime query builder)
204
83
  *
205
- * Reads from the actual TypeScript file in the source directory,
206
- * which enables proper type checking and testability.
84
+ * Reads from the templates directory for proper type checking and testability.
207
85
  */
208
86
  function generateQueryBuilderFile() {
209
- // Read the query-builder.ts source file
210
- // Handle both development (src/) and production (dist/) scenarios
211
- let sourceFilePath = path.join(__dirname, 'query-builder.ts');
212
- // If running from dist/, look for the source in src/ instead
213
- if (!fs.existsSync(sourceFilePath)) {
214
- // Navigate from dist/cli/codegen/orm/ to src/cli/codegen/orm/
215
- sourceFilePath = path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts');
216
- }
217
- // If still not found, try relative to package root
218
- if (!fs.existsSync(sourceFilePath)) {
219
- // For installed packages, the file should be adjacent in the same dir
220
- throw new Error(`Could not find query-builder.ts source file. ` +
221
- `Searched in: ${path.join(__dirname, 'query-builder.ts')} and ` +
222
- `${path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts')}`);
223
- }
224
- let sourceContent = fs.readFileSync(sourceFilePath, 'utf-8');
225
- // Replace the source file header comment with the generated file header
226
- const headerComment = `/**
227
- * Query Builder - Builds and executes GraphQL operations
228
- *
229
- * This is the RUNTIME code that gets copied to generated output.
230
- * It uses gql-ast to build GraphQL documents programmatically.
231
- *
232
- * NOTE: This file is read at codegen time and written to output.
233
- * Any changes here will affect all generated ORM clients.
234
- */`;
235
- const generatedHeader = `/**
236
- * Query Builder - Builds and executes GraphQL operations
237
- * @generated by @constructive-io/graphql-codegen
238
- * DO NOT EDIT - changes will be overwritten
239
- */`;
240
- sourceContent = sourceContent.replace(headerComment, generatedHeader);
241
87
  return {
242
88
  fileName: 'query-builder.ts',
243
- content: sourceContent,
89
+ content: readTemplateFile('query-builder.ts', 'Query Builder - Builds and executes GraphQL operations'),
244
90
  };
245
91
  }
246
92
  /**
247
93
  * Generate the select-types.ts file
248
- */
249
- function generateSelectTypesFile() {
250
- const content = `/**
251
- * Type utilities for select inference
252
- * @generated by @constructive-io/graphql-codegen
253
- * DO NOT EDIT - changes will be overwritten
254
- */
255
-
256
- export interface ConnectionResult<T> {
257
- nodes: T[];
258
- totalCount: number;
259
- pageInfo: PageInfo;
260
- }
261
-
262
- export interface PageInfo {
263
- hasNextPage: boolean;
264
- hasPreviousPage: boolean;
265
- startCursor?: string | null;
266
- endCursor?: string | null;
267
- }
268
-
269
- export interface FindManyArgs<TSelect, TWhere, TOrderBy> {
270
- select?: TSelect;
271
- where?: TWhere;
272
- orderBy?: TOrderBy[];
273
- first?: number;
274
- last?: number;
275
- after?: string;
276
- before?: string;
277
- offset?: number;
278
- }
279
-
280
- export interface FindFirstArgs<TSelect, TWhere> {
281
- select?: TSelect;
282
- where?: TWhere;
283
- }
284
-
285
- export interface CreateArgs<TSelect, TData> {
286
- data: TData;
287
- select?: TSelect;
288
- }
289
-
290
- export interface UpdateArgs<TSelect, TWhere, TData> {
291
- where: TWhere;
292
- data: TData;
293
- select?: TSelect;
294
- }
295
-
296
- export interface DeleteArgs<TWhere> {
297
- where: TWhere;
298
- }
299
-
300
- /**
301
- * Recursively validates select objects, rejecting unknown keys.
302
- *
303
- * This type ensures that users can only select fields that actually exist
304
- * in the GraphQL schema. It returns \`never\` if any excess keys are found
305
- * at any nesting level, causing a TypeScript compile error.
306
- *
307
- * Why this is needed:
308
- * TypeScript's excess property checking has a quirk where it only catches
309
- * invalid fields when they are the ONLY fields. When mixed with valid fields
310
- * (e.g., \`{ id: true, invalidField: true }\`), the structural typing allows
311
- * the excess property through. This type explicitly checks for and rejects
312
- * such cases.
313
- *
314
- * @example
315
- * // This will cause a type error because 'invalid' doesn't exist:
316
- * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
317
- * // Result = never (causes assignment error)
318
94
  *
319
- * @example
320
- * // This works because all fields are valid:
321
- * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
322
- * // Result = { id: true }
323
- */
324
- export type DeepExact<T, Shape> = T extends Shape
325
- ? Exclude<keyof T, keyof Shape> extends never
326
- ? {
327
- [K in keyof T]: K extends keyof Shape
328
- ? T[K] extends { select: infer NS }
329
- ? Shape[K] extends { select?: infer ShapeNS }
330
- ? { select: DeepExact<NS, NonNullable<ShapeNS>> }
331
- : T[K]
332
- : T[K]
333
- : never;
334
- }
335
- : never
336
- : never;
337
-
338
- /**
339
- * Infer result type from select configuration
95
+ * Reads from the templates directory for proper type checking.
340
96
  */
341
- export type InferSelectResult<TEntity, TSelect> = TSelect extends undefined
342
- ? TEntity
343
- : {
344
- [K in keyof TSelect as TSelect[K] extends false | undefined ? never : K]: TSelect[K] extends true
345
- ? K extends keyof TEntity
346
- ? TEntity[K]
347
- : never
348
- : TSelect[K] extends { select: infer NestedSelect }
349
- ? K extends keyof TEntity
350
- ? NonNullable<TEntity[K]> extends ConnectionResult<infer NodeType>
351
- ? ConnectionResult<InferSelectResult<NodeType, NestedSelect>>
352
- : InferSelectResult<NonNullable<TEntity[K]>, NestedSelect> | (null extends TEntity[K] ? null : never)
353
- : never
354
- : K extends keyof TEntity
355
- ? TEntity[K]
356
- : never;
357
- };
358
- `;
97
+ function generateSelectTypesFile() {
359
98
  return {
360
99
  fileName: 'select-types.ts',
361
- content,
100
+ content: readTemplateFile('select-types.ts', 'Type utilities for select inference'),
362
101
  };
363
102
  }
364
103
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
@@ -0,0 +1,265 @@
1
+ /**
2
+ * GraphQL client configuration and execution (Browser-compatible)
3
+ *
4
+ * This is the RUNTIME code that gets copied to generated output.
5
+ * Uses native W3C fetch API for browser compatibility.
6
+ *
7
+ * NOTE: This file is read at codegen time and written to output.
8
+ * Any changes here will affect all generated clients.
9
+ */
10
+
11
+ // ============================================================================
12
+ // Configuration
13
+ // ============================================================================
14
+
15
+ export interface GraphQLClientConfig {
16
+ /** GraphQL endpoint URL */
17
+ endpoint: string;
18
+ /** Default headers to include in all requests */
19
+ headers?: Record<string, string>;
20
+ }
21
+
22
+ let globalConfig: GraphQLClientConfig | null = null;
23
+
24
+ /**
25
+ * Configure the GraphQL client
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { configure } from './generated';
30
+ *
31
+ * configure({
32
+ * endpoint: 'https://api.example.com/graphql',
33
+ * headers: {
34
+ * Authorization: 'Bearer <token>',
35
+ * },
36
+ * });
37
+ * ```
38
+ */
39
+ export function configure(config: GraphQLClientConfig): void {
40
+ globalConfig = config;
41
+ }
42
+
43
+ /**
44
+ * Get the current configuration
45
+ * @throws Error if not configured
46
+ */
47
+ export function getConfig(): GraphQLClientConfig {
48
+ if (!globalConfig) {
49
+ throw new Error(
50
+ 'GraphQL client not configured. Call configure() before making requests.'
51
+ );
52
+ }
53
+ return globalConfig;
54
+ }
55
+
56
+ /**
57
+ * Set a single header value
58
+ * Useful for updating Authorization after login
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * setHeader('Authorization', 'Bearer <new-token>');
63
+ * ```
64
+ */
65
+ export function setHeader(key: string, value: string): void {
66
+ const config = getConfig();
67
+ globalConfig = {
68
+ ...config,
69
+ headers: { ...config.headers, [key]: value },
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Merge multiple headers into the current configuration
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * setHeaders({ Authorization: 'Bearer <token>', 'X-Custom': 'value' });
79
+ * ```
80
+ */
81
+ export function setHeaders(headers: Record<string, string>): void {
82
+ const config = getConfig();
83
+ globalConfig = {
84
+ ...config,
85
+ headers: { ...config.headers, ...headers },
86
+ };
87
+ }
88
+
89
+ // ============================================================================
90
+ // Error handling
91
+ // ============================================================================
92
+
93
+ export interface GraphQLError {
94
+ message: string;
95
+ locations?: Array<{ line: number; column: number }>;
96
+ path?: Array<string | number>;
97
+ extensions?: Record<string, unknown>;
98
+ }
99
+
100
+ export class GraphQLClientError extends Error {
101
+ constructor(
102
+ message: string,
103
+ public errors: GraphQLError[],
104
+ public response?: Response
105
+ ) {
106
+ super(message);
107
+ this.name = 'GraphQLClientError';
108
+ }
109
+ }
110
+
111
+ // ============================================================================
112
+ // Execution
113
+ // ============================================================================
114
+
115
+ export interface ExecuteOptions {
116
+ /** Override headers for this request */
117
+ headers?: Record<string, string>;
118
+ /** AbortSignal for request cancellation */
119
+ signal?: AbortSignal;
120
+ }
121
+
122
+ /**
123
+ * Execute a GraphQL operation
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const result = await execute<CarsQueryResult, CarsQueryVariables>(
128
+ * carsQueryDocument,
129
+ * { first: 10 }
130
+ * );
131
+ * ```
132
+ */
133
+ export async function execute<
134
+ TData = unknown,
135
+ TVariables = Record<string, unknown>,
136
+ >(
137
+ document: string,
138
+ variables?: TVariables,
139
+ options?: ExecuteOptions
140
+ ): Promise<TData> {
141
+ const config = getConfig();
142
+
143
+ const response = await fetch(config.endpoint, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ ...config.headers,
148
+ ...options?.headers,
149
+ },
150
+ body: JSON.stringify({
151
+ query: document,
152
+ variables,
153
+ }),
154
+ signal: options?.signal,
155
+ });
156
+
157
+ const json = await response.json();
158
+
159
+ if (json.errors && json.errors.length > 0) {
160
+ throw new GraphQLClientError(
161
+ json.errors[0].message || 'GraphQL request failed',
162
+ json.errors,
163
+ response
164
+ );
165
+ }
166
+
167
+ return json.data as TData;
168
+ }
169
+
170
+ /**
171
+ * Execute a GraphQL operation with full response (data + errors)
172
+ * Useful when you want to handle partial data with errors
173
+ */
174
+ export async function executeWithErrors<
175
+ TData = unknown,
176
+ TVariables = Record<string, unknown>,
177
+ >(
178
+ document: string,
179
+ variables?: TVariables,
180
+ options?: ExecuteOptions
181
+ ): Promise<{ data: TData | null; errors: GraphQLError[] | null }> {
182
+ const config = getConfig();
183
+
184
+ const response = await fetch(config.endpoint, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ ...config.headers,
189
+ ...options?.headers,
190
+ },
191
+ body: JSON.stringify({
192
+ query: document,
193
+ variables,
194
+ }),
195
+ signal: options?.signal,
196
+ });
197
+
198
+ const json = await response.json();
199
+
200
+ return {
201
+ data: json.data ?? null,
202
+ errors: json.errors ?? null,
203
+ };
204
+ }
205
+
206
+ // ============================================================================
207
+ // QueryClient Factory
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Default QueryClient configuration optimized for GraphQL
212
+ *
213
+ * These defaults provide a good balance between freshness and performance:
214
+ * - staleTime: 1 minute - data considered fresh, won't refetch
215
+ * - gcTime: 5 minutes - unused data kept in cache
216
+ * - refetchOnWindowFocus: false - don't refetch when tab becomes active
217
+ * - retry: 1 - retry failed requests once
218
+ */
219
+ export const defaultQueryClientOptions = {
220
+ defaultOptions: {
221
+ queries: {
222
+ staleTime: 1000 * 60, // 1 minute
223
+ gcTime: 1000 * 60 * 5, // 5 minutes
224
+ refetchOnWindowFocus: false,
225
+ retry: 1,
226
+ },
227
+ },
228
+ };
229
+
230
+ /**
231
+ * QueryClient options type for createQueryClient
232
+ */
233
+ export interface CreateQueryClientOptions {
234
+ defaultOptions?: {
235
+ queries?: {
236
+ staleTime?: number;
237
+ gcTime?: number;
238
+ refetchOnWindowFocus?: boolean;
239
+ retry?: number | boolean;
240
+ retryDelay?: number | ((attemptIndex: number) => number);
241
+ };
242
+ mutations?: {
243
+ retry?: number | boolean;
244
+ retryDelay?: number | ((attemptIndex: number) => number);
245
+ };
246
+ };
247
+ }
248
+
249
+ // Note: createQueryClient is available when using with @tanstack/react-query
250
+ // Import QueryClient from '@tanstack/react-query' and use these options:
251
+ //
252
+ // import { QueryClient } from '@tanstack/react-query';
253
+ // const queryClient = new QueryClient(defaultQueryClientOptions);
254
+ //
255
+ // Or merge with your own options:
256
+ // const queryClient = new QueryClient({
257
+ // ...defaultQueryClientOptions,
258
+ // defaultOptions: {
259
+ // ...defaultQueryClientOptions.defaultOptions,
260
+ // queries: {
261
+ // ...defaultQueryClientOptions.defaultOptions.queries,
262
+ // staleTime: 30000, // Override specific options
263
+ // },
264
+ // },
265
+ // });