@constructive-io/graphql-codegen 3.0.3 → 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.
@@ -4,322 +4,61 @@ import { getTableNames, lcFirst, getGeneratedFileHeader } from '../utils';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  /**
7
- * Generate the main client.ts file (OrmClient class)
8
- * This is the runtime client that handles GraphQL execution
9
- */
10
- export function generateOrmClientFile() {
11
- // This is runtime code that doesn't change based on schema
12
- // We generate it as a static file
13
- const content = `/**
14
- * ORM Client - Runtime GraphQL executor
15
- * @generated by @constructive-io/graphql-codegen
16
- * DO NOT EDIT - changes will be overwritten
17
- */
18
-
19
- import type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
20
-
21
- export type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types';
22
-
23
- /**
24
- * Default adapter that uses fetch for HTTP requests.
25
- * This is used when no custom adapter is provided.
7
+ * Find a template file path.
8
+ * Templates are at ../templates/ relative to this file in both src/ and dist/.
26
9
  */
27
- export class FetchAdapter implements GraphQLAdapter {
28
- private headers: Record<string, string>;
29
-
30
- constructor(
31
- private endpoint: string,
32
- headers?: Record<string, string>
33
- ) {
34
- this.headers = headers ?? {};
35
- }
36
-
37
- async execute<T>(
38
- document: string,
39
- variables?: Record<string, unknown>
40
- ): Promise<QueryResult<T>> {
41
- const response = await fetch(this.endpoint, {
42
- method: 'POST',
43
- headers: {
44
- 'Content-Type': 'application/json',
45
- Accept: 'application/json',
46
- ...this.headers,
47
- },
48
- body: JSON.stringify({
49
- query: document,
50
- variables: variables ?? {},
51
- }),
52
- });
53
-
54
- if (!response.ok) {
55
- return {
56
- ok: false,
57
- data: null,
58
- errors: [{ message: \`HTTP \${response.status}: \${response.statusText}\` }],
59
- };
10
+ function findTemplateFile(templateName) {
11
+ const templatePath = path.join(__dirname, '../templates', templateName);
12
+ if (fs.existsSync(templatePath)) {
13
+ return templatePath;
60
14
  }
61
-
62
- const json = (await response.json()) as {
63
- data?: T;
64
- errors?: GraphQLError[];
65
- };
66
-
67
- if (json.errors && json.errors.length > 0) {
68
- return {
69
- ok: false,
70
- data: null,
71
- errors: json.errors,
72
- };
73
- }
74
-
75
- return {
76
- ok: true,
77
- data: json.data as T,
78
- errors: undefined,
79
- };
80
- }
81
-
82
- setHeaders(headers: Record<string, string>): void {
83
- this.headers = { ...this.headers, ...headers };
84
- }
85
-
86
- getEndpoint(): string {
87
- return this.endpoint;
88
- }
15
+ throw new Error(`Could not find template file: ${templateName}. ` +
16
+ `Searched in: ${templatePath}`);
89
17
  }
90
-
91
18
  /**
92
- * Configuration for creating an ORM client.
93
- * Either provide endpoint (and optional headers) for HTTP requests,
94
- * or provide a custom adapter for alternative execution strategies.
19
+ * Read a template file and replace the header with generated file header
95
20
  */
96
- export interface OrmClientConfig {
97
- /** GraphQL endpoint URL (required if adapter not provided) */
98
- endpoint?: string;
99
- /** Default headers for HTTP requests (only used with endpoint) */
100
- headers?: Record<string, string>;
101
- /** Custom adapter for GraphQL execution (overrides endpoint/headers) */
102
- adapter?: GraphQLAdapter;
21
+ function readTemplateFile(templateName, description) {
22
+ const templatePath = findTemplateFile(templateName);
23
+ let content = fs.readFileSync(templatePath, 'utf-8');
24
+ // Replace the source file header comment with the generated file header
25
+ // Match the header pattern used in template files
26
+ const headerPattern = /\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
27
+ content = content.replace(headerPattern, getGeneratedFileHeader(description) + '\n');
28
+ return content;
103
29
  }
104
-
105
30
  /**
106
- * Error thrown when GraphQL request fails
31
+ * Generate the main client.ts file (OrmClient class)
32
+ * This is the runtime client that handles GraphQL execution
33
+ *
34
+ * Reads from the templates directory for proper type checking.
107
35
  */
108
- export class GraphQLRequestError extends Error {
109
- constructor(
110
- public readonly errors: GraphQLError[],
111
- public readonly data: unknown = null
112
- ) {
113
- const messages = errors.map(e => e.message).join('; ');
114
- super(\`GraphQL Error: \${messages}\`);
115
- this.name = 'GraphQLRequestError';
116
- }
117
- }
118
-
119
- export class OrmClient {
120
- private adapter: GraphQLAdapter;
121
-
122
- constructor(config: OrmClientConfig) {
123
- if (config.adapter) {
124
- this.adapter = config.adapter;
125
- } else if (config.endpoint) {
126
- this.adapter = new FetchAdapter(config.endpoint, config.headers);
127
- } else {
128
- throw new Error('OrmClientConfig requires either an endpoint or a custom adapter');
129
- }
130
- }
131
-
132
- async execute<T>(
133
- document: string,
134
- variables?: Record<string, unknown>
135
- ): Promise<QueryResult<T>> {
136
- return this.adapter.execute<T>(document, variables);
137
- }
138
-
139
- /**
140
- * Set headers for requests.
141
- * Only works if the adapter supports headers.
142
- */
143
- setHeaders(headers: Record<string, string>): void {
144
- if (this.adapter.setHeaders) {
145
- this.adapter.setHeaders(headers);
146
- }
147
- }
148
-
149
- /**
150
- * Get the endpoint URL.
151
- * Returns empty string if the adapter doesn't have an endpoint.
152
- */
153
- getEndpoint(): string {
154
- return this.adapter.getEndpoint?.() ?? '';
155
- }
156
- }
157
- `;
36
+ export function generateOrmClientFile() {
158
37
  return {
159
38
  fileName: 'client.ts',
160
- content,
39
+ content: readTemplateFile('orm-client.ts', 'ORM Client - Runtime GraphQL executor'),
161
40
  };
162
41
  }
163
42
  /**
164
43
  * Generate the query-builder.ts file (runtime query builder)
165
44
  *
166
- * Reads from the actual TypeScript file in the source directory,
167
- * which enables proper type checking and testability.
45
+ * Reads from the templates directory for proper type checking and testability.
168
46
  */
169
47
  export function generateQueryBuilderFile() {
170
- // Read the query-builder.ts source file
171
- // Handle both development (src/) and production (dist/) scenarios
172
- let sourceFilePath = path.join(__dirname, 'query-builder.ts');
173
- // If running from dist/, look for the source in src/ instead
174
- if (!fs.existsSync(sourceFilePath)) {
175
- // Navigate from dist/cli/codegen/orm/ to src/cli/codegen/orm/
176
- sourceFilePath = path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts');
177
- }
178
- // If still not found, try relative to package root
179
- if (!fs.existsSync(sourceFilePath)) {
180
- // For installed packages, the file should be adjacent in the same dir
181
- throw new Error(`Could not find query-builder.ts source file. ` +
182
- `Searched in: ${path.join(__dirname, 'query-builder.ts')} and ` +
183
- `${path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts')}`);
184
- }
185
- let sourceContent = fs.readFileSync(sourceFilePath, 'utf-8');
186
- // Replace the source file header comment with the generated file header
187
- const headerComment = `/**
188
- * Query Builder - Builds and executes GraphQL operations
189
- *
190
- * This is the RUNTIME code that gets copied to generated output.
191
- * It uses gql-ast to build GraphQL documents programmatically.
192
- *
193
- * NOTE: This file is read at codegen time and written to output.
194
- * Any changes here will affect all generated ORM clients.
195
- */`;
196
- const generatedHeader = `/**
197
- * Query Builder - Builds and executes GraphQL operations
198
- * @generated by @constructive-io/graphql-codegen
199
- * DO NOT EDIT - changes will be overwritten
200
- */`;
201
- sourceContent = sourceContent.replace(headerComment, generatedHeader);
202
48
  return {
203
49
  fileName: 'query-builder.ts',
204
- content: sourceContent,
50
+ content: readTemplateFile('query-builder.ts', 'Query Builder - Builds and executes GraphQL operations'),
205
51
  };
206
52
  }
207
53
  /**
208
54
  * Generate the select-types.ts file
209
- */
210
- export function generateSelectTypesFile() {
211
- const content = `/**
212
- * Type utilities for select inference
213
- * @generated by @constructive-io/graphql-codegen
214
- * DO NOT EDIT - changes will be overwritten
215
- */
216
-
217
- export interface ConnectionResult<T> {
218
- nodes: T[];
219
- totalCount: number;
220
- pageInfo: PageInfo;
221
- }
222
-
223
- export interface PageInfo {
224
- hasNextPage: boolean;
225
- hasPreviousPage: boolean;
226
- startCursor?: string | null;
227
- endCursor?: string | null;
228
- }
229
-
230
- export interface FindManyArgs<TSelect, TWhere, TOrderBy> {
231
- select?: TSelect;
232
- where?: TWhere;
233
- orderBy?: TOrderBy[];
234
- first?: number;
235
- last?: number;
236
- after?: string;
237
- before?: string;
238
- offset?: number;
239
- }
240
-
241
- export interface FindFirstArgs<TSelect, TWhere> {
242
- select?: TSelect;
243
- where?: TWhere;
244
- }
245
-
246
- export interface CreateArgs<TSelect, TData> {
247
- data: TData;
248
- select?: TSelect;
249
- }
250
-
251
- export interface UpdateArgs<TSelect, TWhere, TData> {
252
- where: TWhere;
253
- data: TData;
254
- select?: TSelect;
255
- }
256
-
257
- export interface DeleteArgs<TWhere> {
258
- where: TWhere;
259
- }
260
-
261
- /**
262
- * Recursively validates select objects, rejecting unknown keys.
263
- *
264
- * This type ensures that users can only select fields that actually exist
265
- * in the GraphQL schema. It returns \`never\` if any excess keys are found
266
- * at any nesting level, causing a TypeScript compile error.
267
- *
268
- * Why this is needed:
269
- * TypeScript's excess property checking has a quirk where it only catches
270
- * invalid fields when they are the ONLY fields. When mixed with valid fields
271
- * (e.g., \`{ id: true, invalidField: true }\`), the structural typing allows
272
- * the excess property through. This type explicitly checks for and rejects
273
- * such cases.
274
- *
275
- * @example
276
- * // This will cause a type error because 'invalid' doesn't exist:
277
- * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
278
- * // Result = never (causes assignment error)
279
55
  *
280
- * @example
281
- * // This works because all fields are valid:
282
- * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
283
- * // Result = { id: true }
284
- */
285
- export type DeepExact<T, Shape> = T extends Shape
286
- ? Exclude<keyof T, keyof Shape> extends never
287
- ? {
288
- [K in keyof T]: K extends keyof Shape
289
- ? T[K] extends { select: infer NS }
290
- ? Shape[K] extends { select?: infer ShapeNS }
291
- ? { select: DeepExact<NS, NonNullable<ShapeNS>> }
292
- : T[K]
293
- : T[K]
294
- : never;
295
- }
296
- : never
297
- : never;
298
-
299
- /**
300
- * Infer result type from select configuration
56
+ * Reads from the templates directory for proper type checking.
301
57
  */
302
- export type InferSelectResult<TEntity, TSelect> = TSelect extends undefined
303
- ? TEntity
304
- : {
305
- [K in keyof TSelect as TSelect[K] extends false | undefined ? never : K]: TSelect[K] extends true
306
- ? K extends keyof TEntity
307
- ? TEntity[K]
308
- : never
309
- : TSelect[K] extends { select: infer NestedSelect }
310
- ? K extends keyof TEntity
311
- ? NonNullable<TEntity[K]> extends ConnectionResult<infer NodeType>
312
- ? ConnectionResult<InferSelectResult<NodeType, NestedSelect>>
313
- : InferSelectResult<NonNullable<TEntity[K]>, NestedSelect> | (null extends TEntity[K] ? null : never)
314
- : never
315
- : K extends keyof TEntity
316
- ? TEntity[K]
317
- : never;
318
- };
319
- `;
58
+ export function generateSelectTypesFile() {
320
59
  return {
321
60
  fileName: 'select-types.ts',
322
- content,
61
+ content: readTemplateFile('select-types.ts', 'Type utilities for select inference'),
323
62
  };
324
63
  }
325
64
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
@@ -8,6 +8,7 @@ import path from 'path';
8
8
  import { createSchemaSource, validateSourceOptions } from './introspect';
9
9
  import { runCodegenPipeline, validateTablesFound } from './pipeline';
10
10
  import { generate as generateReactQueryFiles } from './codegen';
11
+ import { generateRootBarrel } from './codegen/barrel';
11
12
  import { generateOrm as generateOrmFiles } from './codegen/orm';
12
13
  import { generateSharedTypes } from './codegen/shared';
13
14
  import { writeGeneratedFiles } from './output';
@@ -161,16 +162,14 @@ export async function generate(options = {}) {
161
162
  allFilesWritten.push(...(writeResult.filesWritten ?? []));
162
163
  }
163
164
  }
164
- // Generate unified barrel when both are enabled
165
- if (bothEnabled && !options.dryRun) {
166
- const barrelContent = `/**
167
- * Generated SDK - auto-generated, do not edit
168
- * @generated by @constructive-io/graphql-codegen
169
- */
170
- export * from './types';
171
- export * from './hooks';
172
- export * from './orm';
173
- `;
165
+ // Generate barrel file at output root
166
+ // This re-exports from the appropriate subdirectories based on which generators are enabled
167
+ if (!options.dryRun) {
168
+ const barrelContent = generateRootBarrel({
169
+ hasTypes: bothEnabled,
170
+ hasHooks: runReactQuery,
171
+ hasOrm: runOrm,
172
+ });
174
173
  await writeGeneratedFiles([{ path: 'index.ts', content: barrelContent }], outputRoot, []);
175
174
  }
176
175
  const generators = [runReactQuery && 'React Query', runOrm && 'ORM'].filter(Boolean).join(' and ');
@@ -2,7 +2,7 @@
2
2
  * Fetch GraphQL schema introspection from an endpoint
3
3
  */
4
4
  import dns from 'node:dns';
5
- import { Agent } from 'undici';
5
+ import { Agent, fetch } from 'undici';
6
6
  import { SCHEMA_INTROSPECTION_QUERY } from './schema-query';
7
7
  /**
8
8
  * Check if a hostname is localhost or a localhost subdomain
@@ -20,7 +20,14 @@ function createLocalhostAgent() {
20
20
  connect: {
21
21
  lookup(hostname, opts, cb) {
22
22
  if (isLocalhostHostname(hostname)) {
23
- cb(null, '127.0.0.1', 4);
23
+ // When opts.all is true, callback expects an array of {address, family} objects
24
+ // When opts.all is false/undefined, callback expects (err, address, family)
25
+ if (opts.all) {
26
+ cb(null, [{ address: '127.0.0.1', family: 4 }]);
27
+ }
28
+ else {
29
+ cb(null, '127.0.0.1', 4);
30
+ }
24
31
  return;
25
32
  }
26
33
  dns.lookup(hostname, opts, cb);
@@ -59,7 +66,7 @@ export async function fetchSchema(options) {
59
66
  // Create abort controller for timeout
60
67
  const controller = new AbortController();
61
68
  const timeoutId = setTimeout(() => controller.abort(), timeout);
62
- // Build fetch options
69
+ // Build fetch options using undici's RequestInit type
63
70
  const fetchOptions = {
64
71
  method: 'POST',
65
72
  headers: requestHeaders,
@@ -223,6 +223,14 @@ export interface GraphQLSDKConfigTarget {
223
223
  * @default false
224
224
  */
225
225
  reactQuery?: boolean;
226
+ /**
227
+ * Generate browser-compatible code using native fetch
228
+ * When true (default), uses native W3C fetch API (works in browsers and Node.js)
229
+ * When false, uses undici fetch with dispatcher support for localhost DNS resolution
230
+ * (Node.js only - enables proper *.localhost subdomain resolution on macOS)
231
+ * @default true
232
+ */
233
+ browserCompatible?: boolean;
226
234
  /**
227
235
  * Query key generation configuration
228
236
  * Controls how query keys are structured for cache management
@@ -64,6 +64,7 @@ export const DEFAULT_CONFIG = {
64
64
  },
65
65
  orm: false,
66
66
  reactQuery: false,
67
+ browserCompatible: true,
67
68
  queryKeys: DEFAULT_QUERY_KEY_CONFIG,
68
69
  watch: DEFAULT_WATCH_CONFIG,
69
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -35,9 +35,9 @@
35
35
  "scripts": {
36
36
  "clean": "makage clean",
37
37
  "prepack": "npm run build",
38
- "copy:ts": "makage copy src/core/codegen/orm/query-builder.ts dist/core/codegen/orm --flat",
39
- "build": "makage build && npm run copy:ts",
40
- "build:dev": "makage build --dev && npm run copy:ts",
38
+ "copy:templates": "mkdir -p dist/core/codegen/templates && cp src/core/codegen/templates/*.ts dist/core/codegen/templates/",
39
+ "build": "makage build && npm run copy:templates",
40
+ "build:dev": "makage build --dev && npm run copy:templates",
41
41
  "dev": "ts-node ./src/index.ts",
42
42
  "lint": "eslint . --fix",
43
43
  "fmt": "oxfmt --write .",
@@ -99,5 +99,5 @@
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3"
101
101
  },
102
- "gitHead": "532b9bae03b2a77fd5dff4981ad2a775bb27397a"
102
+ "gitHead": "feaeaaeb78656bccab7725854fb1cfc4e724ba0a"
103
103
  }
package/types/config.d.ts CHANGED
@@ -223,6 +223,14 @@ export interface GraphQLSDKConfigTarget {
223
223
  * @default false
224
224
  */
225
225
  reactQuery?: boolean;
226
+ /**
227
+ * Generate browser-compatible code using native fetch
228
+ * When true (default), uses native W3C fetch API (works in browsers and Node.js)
229
+ * When false, uses undici fetch with dispatcher support for localhost DNS resolution
230
+ * (Node.js only - enables proper *.localhost subdomain resolution on macOS)
231
+ * @default true
232
+ */
233
+ browserCompatible?: boolean;
226
234
  /**
227
235
  * Query key generation configuration
228
236
  * Controls how query keys are structured for cache management
package/types/config.js CHANGED
@@ -73,6 +73,7 @@ exports.DEFAULT_CONFIG = {
73
73
  },
74
74
  orm: false,
75
75
  reactQuery: false,
76
+ browserCompatible: true,
76
77
  queryKeys: exports.DEFAULT_QUERY_KEY_CONFIG,
77
78
  watch: exports.DEFAULT_WATCH_CONFIG,
78
79
  };
@@ -1,85 +0,0 @@
1
- /**
2
- * Query Builder - Builds and executes GraphQL operations
3
- *
4
- * This is the RUNTIME code that gets copied to generated output.
5
- * It uses gql-ast to build GraphQL documents programmatically.
6
- *
7
- * NOTE: This file is read at codegen time and written to output.
8
- * Any changes here will affect all generated ORM clients.
9
- */
10
- import type { FieldNode } from 'graphql';
11
- import { OrmClient, QueryResult } from './client';
12
- export interface QueryBuilderConfig {
13
- client: OrmClient;
14
- operation: 'query' | 'mutation';
15
- operationName: string;
16
- fieldName: string;
17
- document: string;
18
- variables?: Record<string, unknown>;
19
- }
20
- export declare class QueryBuilder<TResult> {
21
- private config;
22
- constructor(config: QueryBuilderConfig);
23
- /**
24
- * Execute the query and return a discriminated union result
25
- * Use result.ok to check success, or .unwrap() to throw on error
26
- */
27
- execute(): Promise<QueryResult<TResult>>;
28
- /**
29
- * Execute and unwrap the result, throwing GraphQLRequestError on failure
30
- * @throws {GraphQLRequestError} If the query returns errors
31
- */
32
- unwrap(): Promise<TResult>;
33
- /**
34
- * Execute and unwrap, returning defaultValue on error instead of throwing
35
- */
36
- unwrapOr<D>(defaultValue: D): Promise<TResult | D>;
37
- /**
38
- * Execute and unwrap, calling onError callback on failure
39
- */
40
- unwrapOrElse<D>(onError: (errors: import('./client').GraphQLError[]) => D): Promise<TResult | D>;
41
- toGraphQL(): string;
42
- getVariables(): Record<string, unknown> | undefined;
43
- }
44
- export declare function buildSelections(select: Record<string, unknown> | undefined): FieldNode[];
45
- export declare function buildFindManyDocument<TSelect, TWhere>(operationName: string, queryField: string, select: TSelect, args: {
46
- where?: TWhere;
47
- orderBy?: string[];
48
- first?: number;
49
- last?: number;
50
- after?: string;
51
- before?: string;
52
- offset?: number;
53
- }, filterTypeName: string, orderByTypeName: string): {
54
- document: string;
55
- variables: Record<string, unknown>;
56
- };
57
- export declare function buildFindFirstDocument<TSelect, TWhere>(operationName: string, queryField: string, select: TSelect, args: {
58
- where?: TWhere;
59
- }, filterTypeName: string): {
60
- document: string;
61
- variables: Record<string, unknown>;
62
- };
63
- export declare function buildCreateDocument<TSelect, TData>(operationName: string, mutationField: string, entityField: string, select: TSelect, data: TData, inputTypeName: string): {
64
- document: string;
65
- variables: Record<string, unknown>;
66
- };
67
- export declare function buildUpdateDocument<TSelect, TWhere extends {
68
- id: string;
69
- }, TData>(operationName: string, mutationField: string, entityField: string, select: TSelect, where: TWhere, data: TData, inputTypeName: string): {
70
- document: string;
71
- variables: Record<string, unknown>;
72
- };
73
- export declare function buildDeleteDocument<TWhere extends {
74
- id: string;
75
- }>(operationName: string, mutationField: string, entityField: string, where: TWhere, inputTypeName: string): {
76
- document: string;
77
- variables: Record<string, unknown>;
78
- };
79
- export declare function buildCustomDocument<TSelect, TArgs>(operationType: 'query' | 'mutation', operationName: string, fieldName: string, select: TSelect, args: TArgs, variableDefinitions: Array<{
80
- name: string;
81
- type: string;
82
- }>): {
83
- document: string;
84
- variables: Record<string, unknown>;
85
- };