@constructive-io/graphql-codegen 3.0.4 → 3.1.1
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.
- package/README.md +4 -0
- package/cli/index.js +4 -1
- package/cli/shared.d.ts +1 -0
- package/cli/shared.js +8 -0
- package/core/codegen/client.d.ts +11 -1
- package/core/codegen/client.js +63 -248
- package/core/codegen/index.js +3 -1
- package/core/codegen/orm/client-generator.d.ts +5 -2
- package/core/codegen/orm/client-generator.js +28 -289
- package/core/codegen/templates/client.browser.ts +265 -0
- package/core/codegen/templates/client.node.ts +350 -0
- package/core/codegen/templates/orm-client.ts +158 -0
- package/core/codegen/templates/select-types.ts +116 -0
- package/core/introspect/fetch-schema.js +10 -3
- package/esm/cli/index.js +4 -1
- package/esm/cli/shared.d.ts +1 -0
- package/esm/cli/shared.js +8 -0
- package/esm/core/codegen/client.d.ts +11 -1
- package/esm/core/codegen/client.js +31 -249
- package/esm/core/codegen/index.js +3 -1
- package/esm/core/codegen/orm/client-generator.d.ts +5 -2
- package/esm/core/codegen/orm/client-generator.js +28 -289
- package/esm/core/introspect/fetch-schema.js +10 -3
- package/esm/types/config.d.ts +8 -0
- package/esm/types/config.js +1 -0
- package/package.json +5 -5
- package/types/config.d.ts +8 -0
- package/types/config.js +1 -0
- package/core/codegen/orm/query-builder.d.ts +0 -85
- package/core/codegen/orm/query-builder.js +0 -485
- package/esm/core/codegen/orm/query-builder.d.ts +0 -85
- package/esm/core/codegen/orm/query-builder.js +0 -441
- /package/core/codegen/{orm → templates}/query-builder.ts +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL client configuration and execution (Node.js with undici)
|
|
3
|
+
*
|
|
4
|
+
* This is the RUNTIME code that gets copied to generated output.
|
|
5
|
+
* Uses undici fetch with dispatcher support for localhost DNS resolution.
|
|
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
|
+
import dns from 'node:dns';
|
|
12
|
+
import { Agent, fetch, type RequestInit } from 'undici';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Localhost DNS Resolution
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a hostname is localhost or a localhost subdomain
|
|
20
|
+
*/
|
|
21
|
+
function isLocalhostHostname(hostname: string): boolean {
|
|
22
|
+
return hostname === 'localhost' || hostname.endsWith('.localhost');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an undici Agent that resolves *.localhost to 127.0.0.1
|
|
27
|
+
* This fixes DNS resolution issues on macOS where subdomains like api.localhost
|
|
28
|
+
* don't resolve automatically (unlike browsers which handle *.localhost).
|
|
29
|
+
*/
|
|
30
|
+
function createLocalhostAgent(): Agent {
|
|
31
|
+
return new Agent({
|
|
32
|
+
connect: {
|
|
33
|
+
lookup(hostname, opts, cb) {
|
|
34
|
+
if (isLocalhostHostname(hostname)) {
|
|
35
|
+
// When opts.all is true, callback expects an array of {address, family} objects
|
|
36
|
+
// When opts.all is false/undefined, callback expects (err, address, family)
|
|
37
|
+
if (opts.all) {
|
|
38
|
+
cb(null, [{ address: '127.0.0.1', family: 4 }]);
|
|
39
|
+
} else {
|
|
40
|
+
cb(null, '127.0.0.1', 4);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
dns.lookup(hostname, opts, cb);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let localhostAgent: Agent | null = null;
|
|
51
|
+
|
|
52
|
+
function getLocalhostAgent(): Agent {
|
|
53
|
+
if (!localhostAgent) {
|
|
54
|
+
localhostAgent = createLocalhostAgent();
|
|
55
|
+
}
|
|
56
|
+
return localhostAgent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get fetch options with localhost agent if needed
|
|
61
|
+
*/
|
|
62
|
+
function getFetchOptions(
|
|
63
|
+
endpoint: string,
|
|
64
|
+
baseOptions: RequestInit
|
|
65
|
+
): RequestInit {
|
|
66
|
+
const url = new URL(endpoint);
|
|
67
|
+
if (isLocalhostHostname(url.hostname)) {
|
|
68
|
+
const options: RequestInit = {
|
|
69
|
+
...baseOptions,
|
|
70
|
+
dispatcher: getLocalhostAgent(),
|
|
71
|
+
};
|
|
72
|
+
// Set Host header for localhost subdomains to preserve routing
|
|
73
|
+
if (url.hostname !== 'localhost') {
|
|
74
|
+
options.headers = {
|
|
75
|
+
...(baseOptions.headers as Record<string, string>),
|
|
76
|
+
Host: url.hostname,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
return baseOptions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Configuration
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export interface GraphQLClientConfig {
|
|
89
|
+
/** GraphQL endpoint URL */
|
|
90
|
+
endpoint: string;
|
|
91
|
+
/** Default headers to include in all requests */
|
|
92
|
+
headers?: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let globalConfig: GraphQLClientConfig | null = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Configure the GraphQL client
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { configure } from './generated';
|
|
103
|
+
*
|
|
104
|
+
* configure({
|
|
105
|
+
* endpoint: 'https://api.example.com/graphql',
|
|
106
|
+
* headers: {
|
|
107
|
+
* Authorization: 'Bearer <token>',
|
|
108
|
+
* },
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function configure(config: GraphQLClientConfig): void {
|
|
113
|
+
globalConfig = config;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the current configuration
|
|
118
|
+
* @throws Error if not configured
|
|
119
|
+
*/
|
|
120
|
+
export function getConfig(): GraphQLClientConfig {
|
|
121
|
+
if (!globalConfig) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'GraphQL client not configured. Call configure() before making requests.'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return globalConfig;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set a single header value
|
|
131
|
+
* Useful for updating Authorization after login
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* setHeader('Authorization', 'Bearer <new-token>');
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function setHeader(key: string, value: string): void {
|
|
139
|
+
const config = getConfig();
|
|
140
|
+
globalConfig = {
|
|
141
|
+
...config,
|
|
142
|
+
headers: { ...config.headers, [key]: value },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Merge multiple headers into the current configuration
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* setHeaders({ Authorization: 'Bearer <token>', 'X-Custom': 'value' });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function setHeaders(headers: Record<string, string>): void {
|
|
155
|
+
const config = getConfig();
|
|
156
|
+
globalConfig = {
|
|
157
|
+
...config,
|
|
158
|
+
headers: { ...config.headers, ...headers },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Error handling
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
export interface GraphQLError {
|
|
167
|
+
message: string;
|
|
168
|
+
locations?: Array<{ line: number; column: number }>;
|
|
169
|
+
path?: Array<string | number>;
|
|
170
|
+
extensions?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class GraphQLClientError extends Error {
|
|
174
|
+
constructor(
|
|
175
|
+
message: string,
|
|
176
|
+
public errors: GraphQLError[],
|
|
177
|
+
public response?: Response
|
|
178
|
+
) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.name = 'GraphQLClientError';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Execution
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
export interface ExecuteOptions {
|
|
189
|
+
/** Override headers for this request */
|
|
190
|
+
headers?: Record<string, string>;
|
|
191
|
+
/** AbortSignal for request cancellation */
|
|
192
|
+
signal?: AbortSignal;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Execute a GraphQL operation
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const result = await execute<CarsQueryResult, CarsQueryVariables>(
|
|
201
|
+
* carsQueryDocument,
|
|
202
|
+
* { first: 10 }
|
|
203
|
+
* );
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export async function execute<
|
|
207
|
+
TData = unknown,
|
|
208
|
+
TVariables = Record<string, unknown>,
|
|
209
|
+
>(
|
|
210
|
+
document: string,
|
|
211
|
+
variables?: TVariables,
|
|
212
|
+
options?: ExecuteOptions
|
|
213
|
+
): Promise<TData> {
|
|
214
|
+
const config = getConfig();
|
|
215
|
+
|
|
216
|
+
const baseOptions: RequestInit = {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
...config.headers,
|
|
221
|
+
...options?.headers,
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
query: document,
|
|
225
|
+
variables,
|
|
226
|
+
}),
|
|
227
|
+
signal: options?.signal,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const fetchOptions = getFetchOptions(config.endpoint, baseOptions);
|
|
231
|
+
const response = await fetch(config.endpoint, fetchOptions);
|
|
232
|
+
|
|
233
|
+
const json = (await response.json()) as {
|
|
234
|
+
data?: TData;
|
|
235
|
+
errors?: GraphQLError[];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (json.errors && json.errors.length > 0) {
|
|
239
|
+
throw new GraphQLClientError(
|
|
240
|
+
json.errors[0].message || 'GraphQL request failed',
|
|
241
|
+
json.errors,
|
|
242
|
+
response as unknown as Response
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return json.data as TData;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Execute a GraphQL operation with full response (data + errors)
|
|
251
|
+
* Useful when you want to handle partial data with errors
|
|
252
|
+
*/
|
|
253
|
+
export async function executeWithErrors<
|
|
254
|
+
TData = unknown,
|
|
255
|
+
TVariables = Record<string, unknown>,
|
|
256
|
+
>(
|
|
257
|
+
document: string,
|
|
258
|
+
variables?: TVariables,
|
|
259
|
+
options?: ExecuteOptions
|
|
260
|
+
): Promise<{ data: TData | null; errors: GraphQLError[] | null }> {
|
|
261
|
+
const config = getConfig();
|
|
262
|
+
|
|
263
|
+
const baseOptions: RequestInit = {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
...config.headers,
|
|
268
|
+
...options?.headers,
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
query: document,
|
|
272
|
+
variables,
|
|
273
|
+
}),
|
|
274
|
+
signal: options?.signal,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const fetchOptions = getFetchOptions(config.endpoint, baseOptions);
|
|
278
|
+
const response = await fetch(config.endpoint, fetchOptions);
|
|
279
|
+
|
|
280
|
+
const json = (await response.json()) as {
|
|
281
|
+
data?: TData;
|
|
282
|
+
errors?: GraphQLError[];
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
data: json.data ?? null,
|
|
287
|
+
errors: json.errors ?? null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// QueryClient Factory
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Default QueryClient configuration optimized for GraphQL
|
|
297
|
+
*
|
|
298
|
+
* These defaults provide a good balance between freshness and performance:
|
|
299
|
+
* - staleTime: 1 minute - data considered fresh, won't refetch
|
|
300
|
+
* - gcTime: 5 minutes - unused data kept in cache
|
|
301
|
+
* - refetchOnWindowFocus: false - don't refetch when tab becomes active
|
|
302
|
+
* - retry: 1 - retry failed requests once
|
|
303
|
+
*/
|
|
304
|
+
export const defaultQueryClientOptions = {
|
|
305
|
+
defaultOptions: {
|
|
306
|
+
queries: {
|
|
307
|
+
staleTime: 1000 * 60, // 1 minute
|
|
308
|
+
gcTime: 1000 * 60 * 5, // 5 minutes
|
|
309
|
+
refetchOnWindowFocus: false,
|
|
310
|
+
retry: 1,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* QueryClient options type for createQueryClient
|
|
317
|
+
*/
|
|
318
|
+
export interface CreateQueryClientOptions {
|
|
319
|
+
defaultOptions?: {
|
|
320
|
+
queries?: {
|
|
321
|
+
staleTime?: number;
|
|
322
|
+
gcTime?: number;
|
|
323
|
+
refetchOnWindowFocus?: boolean;
|
|
324
|
+
retry?: number | boolean;
|
|
325
|
+
retryDelay?: number | ((attemptIndex: number) => number);
|
|
326
|
+
};
|
|
327
|
+
mutations?: {
|
|
328
|
+
retry?: number | boolean;
|
|
329
|
+
retryDelay?: number | ((attemptIndex: number) => number);
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Note: createQueryClient is available when using with @tanstack/react-query
|
|
335
|
+
// Import QueryClient from '@tanstack/react-query' and use these options:
|
|
336
|
+
//
|
|
337
|
+
// import { QueryClient } from '@tanstack/react-query';
|
|
338
|
+
// const queryClient = new QueryClient(defaultQueryClientOptions);
|
|
339
|
+
//
|
|
340
|
+
// Or merge with your own options:
|
|
341
|
+
// const queryClient = new QueryClient({
|
|
342
|
+
// ...defaultQueryClientOptions,
|
|
343
|
+
// defaultOptions: {
|
|
344
|
+
// ...defaultQueryClientOptions.defaultOptions,
|
|
345
|
+
// queries: {
|
|
346
|
+
// ...defaultQueryClientOptions.defaultOptions.queries,
|
|
347
|
+
// staleTime: 30000, // Override specific options
|
|
348
|
+
// },
|
|
349
|
+
// },
|
|
350
|
+
// });
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORM Client - Runtime GraphQL executor
|
|
3
|
+
*
|
|
4
|
+
* This is the RUNTIME code that gets copied to generated output.
|
|
5
|
+
* Provides the core ORM client functionality for executing GraphQL operations.
|
|
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
|
+
|
|
11
|
+
import type {
|
|
12
|
+
GraphQLAdapter,
|
|
13
|
+
GraphQLError,
|
|
14
|
+
QueryResult,
|
|
15
|
+
} from '@constructive-io/graphql-types';
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
GraphQLAdapter,
|
|
19
|
+
GraphQLError,
|
|
20
|
+
QueryResult,
|
|
21
|
+
} 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.
|
|
26
|
+
*/
|
|
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
|
+
};
|
|
60
|
+
}
|
|
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
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.
|
|
95
|
+
*/
|
|
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;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Error thrown when GraphQL request fails
|
|
107
|
+
*/
|
|
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(
|
|
129
|
+
'OrmClientConfig requires either an endpoint or a custom adapter'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async execute<T>(
|
|
135
|
+
document: string,
|
|
136
|
+
variables?: Record<string, unknown>
|
|
137
|
+
): Promise<QueryResult<T>> {
|
|
138
|
+
return this.adapter.execute<T>(document, variables);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set headers for requests.
|
|
143
|
+
* Only works if the adapter supports headers.
|
|
144
|
+
*/
|
|
145
|
+
setHeaders(headers: Record<string, string>): void {
|
|
146
|
+
if (this.adapter.setHeaders) {
|
|
147
|
+
this.adapter.setHeaders(headers);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the endpoint URL.
|
|
153
|
+
* Returns empty string if the adapter doesn't have an endpoint.
|
|
154
|
+
*/
|
|
155
|
+
getEndpoint(): string {
|
|
156
|
+
return this.adapter.getEndpoint?.() ?? '';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type utilities for select inference
|
|
3
|
+
*
|
|
4
|
+
* This is the RUNTIME code that gets copied to generated output.
|
|
5
|
+
* Provides type utilities for ORM select operations.
|
|
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
|
+
|
|
11
|
+
export interface ConnectionResult<T> {
|
|
12
|
+
nodes: T[];
|
|
13
|
+
totalCount: number;
|
|
14
|
+
pageInfo: PageInfo;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PageInfo {
|
|
18
|
+
hasNextPage: boolean;
|
|
19
|
+
hasPreviousPage: boolean;
|
|
20
|
+
startCursor?: string | null;
|
|
21
|
+
endCursor?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FindManyArgs<TSelect, TWhere, TOrderBy> {
|
|
25
|
+
select?: TSelect;
|
|
26
|
+
where?: TWhere;
|
|
27
|
+
orderBy?: TOrderBy[];
|
|
28
|
+
first?: number;
|
|
29
|
+
last?: number;
|
|
30
|
+
after?: string;
|
|
31
|
+
before?: string;
|
|
32
|
+
offset?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FindFirstArgs<TSelect, TWhere> {
|
|
36
|
+
select?: TSelect;
|
|
37
|
+
where?: TWhere;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CreateArgs<TSelect, TData> {
|
|
41
|
+
data: TData;
|
|
42
|
+
select?: TSelect;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UpdateArgs<TSelect, TWhere, TData> {
|
|
46
|
+
where: TWhere;
|
|
47
|
+
data: TData;
|
|
48
|
+
select?: TSelect;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DeleteArgs<TWhere> {
|
|
52
|
+
where: TWhere;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recursively validates select objects, rejecting unknown keys.
|
|
57
|
+
*
|
|
58
|
+
* This type ensures that users can only select fields that actually exist
|
|
59
|
+
* in the GraphQL schema. It returns `never` if any excess keys are found
|
|
60
|
+
* at any nesting level, causing a TypeScript compile error.
|
|
61
|
+
*
|
|
62
|
+
* Why this is needed:
|
|
63
|
+
* TypeScript's excess property checking has a quirk where it only catches
|
|
64
|
+
* invalid fields when they are the ONLY fields. When mixed with valid fields
|
|
65
|
+
* (e.g., `{ id: true, invalidField: true }`), the structural typing allows
|
|
66
|
+
* the excess property through. This type explicitly checks for and rejects
|
|
67
|
+
* such cases.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // This will cause a type error because 'invalid' doesn't exist:
|
|
71
|
+
* type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
|
|
72
|
+
* // Result = never (causes assignment error)
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // This works because all fields are valid:
|
|
76
|
+
* type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
|
|
77
|
+
* // Result = { id: true }
|
|
78
|
+
*/
|
|
79
|
+
export type DeepExact<T, Shape> = T extends Shape
|
|
80
|
+
? Exclude<keyof T, keyof Shape> extends never
|
|
81
|
+
? {
|
|
82
|
+
[K in keyof T]: K extends keyof Shape
|
|
83
|
+
? T[K] extends { select: infer NS }
|
|
84
|
+
? Shape[K] extends { select?: infer ShapeNS }
|
|
85
|
+
? { select: DeepExact<NS, NonNullable<ShapeNS>> }
|
|
86
|
+
: T[K]
|
|
87
|
+
: T[K]
|
|
88
|
+
: never;
|
|
89
|
+
}
|
|
90
|
+
: never
|
|
91
|
+
: never;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Infer result type from select configuration
|
|
95
|
+
*/
|
|
96
|
+
export type InferSelectResult<TEntity, TSelect> = TSelect extends undefined
|
|
97
|
+
? TEntity
|
|
98
|
+
: {
|
|
99
|
+
[K in keyof TSelect as TSelect[K] extends false | undefined
|
|
100
|
+
? never
|
|
101
|
+
: K]: TSelect[K] extends true
|
|
102
|
+
? K extends keyof TEntity
|
|
103
|
+
? TEntity[K]
|
|
104
|
+
: never
|
|
105
|
+
: TSelect[K] extends { select: infer NestedSelect }
|
|
106
|
+
? K extends keyof TEntity
|
|
107
|
+
? NonNullable<TEntity[K]> extends ConnectionResult<infer NodeType>
|
|
108
|
+
? ConnectionResult<InferSelectResult<NodeType, NestedSelect>>
|
|
109
|
+
:
|
|
110
|
+
| InferSelectResult<NonNullable<TEntity[K]>, NestedSelect>
|
|
111
|
+
| (null extends TEntity[K] ? null : never)
|
|
112
|
+
: never
|
|
113
|
+
: K extends keyof TEntity
|
|
114
|
+
? TEntity[K]
|
|
115
|
+
: never;
|
|
116
|
+
};
|
|
@@ -26,7 +26,14 @@ function createLocalhostAgent() {
|
|
|
26
26
|
connect: {
|
|
27
27
|
lookup(hostname, opts, cb) {
|
|
28
28
|
if (isLocalhostHostname(hostname)) {
|
|
29
|
-
|
|
29
|
+
// When opts.all is true, callback expects an array of {address, family} objects
|
|
30
|
+
// When opts.all is false/undefined, callback expects (err, address, family)
|
|
31
|
+
if (opts.all) {
|
|
32
|
+
cb(null, [{ address: '127.0.0.1', family: 4 }]);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
cb(null, '127.0.0.1', 4);
|
|
36
|
+
}
|
|
30
37
|
return;
|
|
31
38
|
}
|
|
32
39
|
node_dns_1.default.lookup(hostname, opts, cb);
|
|
@@ -65,7 +72,7 @@ async function fetchSchema(options) {
|
|
|
65
72
|
// Create abort controller for timeout
|
|
66
73
|
const controller = new AbortController();
|
|
67
74
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
68
|
-
// Build fetch options
|
|
75
|
+
// Build fetch options using undici's RequestInit type
|
|
69
76
|
const fetchOptions = {
|
|
70
77
|
method: 'POST',
|
|
71
78
|
headers: requestHeaders,
|
|
@@ -80,7 +87,7 @@ async function fetchSchema(options) {
|
|
|
80
87
|
fetchOptions.dispatcher = getLocalhostAgent();
|
|
81
88
|
}
|
|
82
89
|
try {
|
|
83
|
-
const response = await fetch(endpoint, fetchOptions);
|
|
90
|
+
const response = await (0, undici_1.fetch)(endpoint, fetchOptions);
|
|
84
91
|
clearTimeout(timeoutId);
|
|
85
92
|
if (!response.ok) {
|
|
86
93
|
return {
|
package/esm/cli/index.js
CHANGED
|
@@ -30,6 +30,8 @@ Generator Options:
|
|
|
30
30
|
-o, --output <dir> Output directory
|
|
31
31
|
-t, --target <name> Target name (for multi-target configs)
|
|
32
32
|
-a, --authorization <token> Authorization header value
|
|
33
|
+
--browser-compatible Generate browser-compatible code (default: true)
|
|
34
|
+
Set to false for Node.js with localhost DNS fix
|
|
33
35
|
--dry-run Preview without writing files
|
|
34
36
|
-v, --verbose Show detailed output
|
|
35
37
|
|
|
@@ -102,6 +104,7 @@ export const commands = async (argv, prompter, _options) => {
|
|
|
102
104
|
authorization: answers.authorization,
|
|
103
105
|
reactQuery: answers.reactQuery,
|
|
104
106
|
orm: answers.orm,
|
|
107
|
+
browserCompatible: answers.browserCompatible,
|
|
105
108
|
dryRun: answers.dryRun,
|
|
106
109
|
verbose: answers.verbose,
|
|
107
110
|
});
|
|
@@ -122,7 +125,7 @@ export const options = {
|
|
|
122
125
|
v: 'verbose',
|
|
123
126
|
},
|
|
124
127
|
boolean: [
|
|
125
|
-
'help', 'version', 'verbose', 'dry-run', 'react-query', 'orm', 'keep-db',
|
|
128
|
+
'help', 'version', 'verbose', 'dry-run', 'react-query', 'orm', 'keep-db', 'browser-compatible',
|
|
126
129
|
],
|
|
127
130
|
string: [
|
|
128
131
|
'config', 'endpoint', 'schema-file', 'output', 'target', 'authorization',
|
package/esm/cli/shared.d.ts
CHANGED
package/esm/cli/shared.js
CHANGED
|
@@ -60,6 +60,14 @@ export const codegenQuestions = [
|
|
|
60
60
|
default: false,
|
|
61
61
|
useDefault: true,
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: 'browserCompatible',
|
|
65
|
+
message: 'Generate browser-compatible code?',
|
|
66
|
+
type: 'confirm',
|
|
67
|
+
required: false,
|
|
68
|
+
default: true,
|
|
69
|
+
useDefault: true,
|
|
70
|
+
},
|
|
63
71
|
{
|
|
64
72
|
name: 'authorization',
|
|
65
73
|
message: 'Authorization header value',
|