@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
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
export interface GenerateClientFileOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Generate browser-compatible code using native fetch
|
|
4
|
+
* When true (default), uses native W3C fetch API
|
|
5
|
+
* When false, uses undici fetch with dispatcher support for localhost DNS resolution
|
|
6
|
+
* @default true
|
|
7
|
+
*/
|
|
8
|
+
browserCompatible?: boolean;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Generate client.ts content
|
|
12
|
+
* @param options - Generation options
|
|
3
13
|
*/
|
|
4
|
-
export declare function generateClientFile(): string;
|
|
14
|
+
export declare function generateClientFile(options?: GenerateClientFileOptions): string;
|
|
@@ -1,261 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client generator - generates client.ts with configure() and execute()
|
|
3
|
-
*/
|
|
4
|
-
import { getGeneratedFileHeader } from './utils';
|
|
5
|
-
/**
|
|
6
|
-
* Generate client.ts content
|
|
7
|
-
*/
|
|
8
|
-
export function generateClientFile() {
|
|
9
|
-
return `${getGeneratedFileHeader('GraphQL client configuration and execution')}
|
|
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
3
|
*
|
|
31
|
-
*
|
|
32
|
-
* endpoint: 'https://api.example.com/graphql',
|
|
33
|
-
* headers: {
|
|
34
|
-
* Authorization: 'Bearer <token>',
|
|
35
|
-
* },
|
|
36
|
-
* });
|
|
37
|
-
* \`\`\`
|
|
4
|
+
* Reads from template files in the templates/ directory for proper type checking.
|
|
38
5
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getGeneratedFileHeader } from './utils';
|
|
73
9
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* \`\`\`ts
|
|
78
|
-
* setHeaders({ Authorization: 'Bearer <token>', 'X-Custom': 'value' });
|
|
79
|
-
* \`\`\`
|
|
10
|
+
* Find a template file path.
|
|
11
|
+
* Templates are at ./templates/ relative to this file in both src/ and dist/.
|
|
80
12
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
13
|
+
function findTemplateFile(templateName) {
|
|
14
|
+
const templatePath = path.join(__dirname, 'templates', templateName);
|
|
15
|
+
if (fs.existsSync(templatePath)) {
|
|
16
|
+
return templatePath;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Could not find template file: ${templateName}. ` +
|
|
19
|
+
`Searched in: ${templatePath}`);
|
|
109
20
|
}
|
|
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
21
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* @example
|
|
126
|
-
* \`\`\`ts
|
|
127
|
-
* const result = await execute<CarsQueryResult, CarsQueryVariables>(
|
|
128
|
-
* carsQueryDocument,
|
|
129
|
-
* { first: 10 }
|
|
130
|
-
* );
|
|
131
|
-
* \`\`\`
|
|
22
|
+
* Read a template file and replace the header with generated file header
|
|
132
23
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
method: 'POST',
|
|
142
|
-
headers: {
|
|
143
|
-
'Content-Type': 'application/json',
|
|
144
|
-
...config.headers,
|
|
145
|
-
...options?.headers,
|
|
146
|
-
},
|
|
147
|
-
body: JSON.stringify({
|
|
148
|
-
query: document,
|
|
149
|
-
variables,
|
|
150
|
-
}),
|
|
151
|
-
signal: options?.signal,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const json = await response.json();
|
|
155
|
-
|
|
156
|
-
if (json.errors && json.errors.length > 0) {
|
|
157
|
-
throw new GraphQLClientError(
|
|
158
|
-
json.errors[0].message || 'GraphQL request failed',
|
|
159
|
-
json.errors,
|
|
160
|
-
response
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return json.data as TData;
|
|
24
|
+
function readTemplateFile(templateName, description) {
|
|
25
|
+
const templatePath = findTemplateFile(templateName);
|
|
26
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
27
|
+
// Replace the source file header comment with the generated file header
|
|
28
|
+
// Match the header pattern used in template files
|
|
29
|
+
const headerPattern = /\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
|
|
30
|
+
content = content.replace(headerPattern, getGeneratedFileHeader(description) + '\n');
|
|
31
|
+
return content;
|
|
165
32
|
}
|
|
166
|
-
|
|
167
33
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*/
|
|
171
|
-
export
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const response = await fetch(config.endpoint, {
|
|
179
|
-
method: 'POST',
|
|
180
|
-
headers: {
|
|
181
|
-
'Content-Type': 'application/json',
|
|
182
|
-
...config.headers,
|
|
183
|
-
...options?.headers,
|
|
184
|
-
},
|
|
185
|
-
body: JSON.stringify({
|
|
186
|
-
query: document,
|
|
187
|
-
variables,
|
|
188
|
-
}),
|
|
189
|
-
signal: options?.signal,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const json = await response.json();
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
data: json.data ?? null,
|
|
196
|
-
errors: json.errors ?? null,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ============================================================================
|
|
201
|
-
// QueryClient Factory
|
|
202
|
-
// ============================================================================
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Default QueryClient configuration optimized for GraphQL
|
|
206
|
-
*
|
|
207
|
-
* These defaults provide a good balance between freshness and performance:
|
|
208
|
-
* - staleTime: 1 minute - data considered fresh, won't refetch
|
|
209
|
-
* - gcTime: 5 minutes - unused data kept in cache
|
|
210
|
-
* - refetchOnWindowFocus: false - don't refetch when tab becomes active
|
|
211
|
-
* - retry: 1 - retry failed requests once
|
|
212
|
-
*/
|
|
213
|
-
export const defaultQueryClientOptions = {
|
|
214
|
-
defaultOptions: {
|
|
215
|
-
queries: {
|
|
216
|
-
staleTime: 1000 * 60, // 1 minute
|
|
217
|
-
gcTime: 1000 * 60 * 5, // 5 minutes
|
|
218
|
-
refetchOnWindowFocus: false,
|
|
219
|
-
retry: 1,
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* QueryClient options type for createQueryClient
|
|
226
|
-
*/
|
|
227
|
-
export interface CreateQueryClientOptions {
|
|
228
|
-
defaultOptions?: {
|
|
229
|
-
queries?: {
|
|
230
|
-
staleTime?: number;
|
|
231
|
-
gcTime?: number;
|
|
232
|
-
refetchOnWindowFocus?: boolean;
|
|
233
|
-
retry?: number | boolean;
|
|
234
|
-
retryDelay?: number | ((attemptIndex: number) => number);
|
|
235
|
-
};
|
|
236
|
-
mutations?: {
|
|
237
|
-
retry?: number | boolean;
|
|
238
|
-
retryDelay?: number | ((attemptIndex: number) => number);
|
|
239
|
-
};
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Note: createQueryClient is available when using with @tanstack/react-query
|
|
244
|
-
// Import QueryClient from '@tanstack/react-query' and use these options:
|
|
245
|
-
//
|
|
246
|
-
// import { QueryClient } from '@tanstack/react-query';
|
|
247
|
-
// const queryClient = new QueryClient(defaultQueryClientOptions);
|
|
248
|
-
//
|
|
249
|
-
// Or merge with your own options:
|
|
250
|
-
// const queryClient = new QueryClient({
|
|
251
|
-
// ...defaultQueryClientOptions,
|
|
252
|
-
// defaultOptions: {
|
|
253
|
-
// ...defaultQueryClientOptions.defaultOptions,
|
|
254
|
-
// queries: {
|
|
255
|
-
// ...defaultQueryClientOptions.defaultOptions.queries,
|
|
256
|
-
// staleTime: 30000, // Override specific options
|
|
257
|
-
// },
|
|
258
|
-
// },
|
|
259
|
-
// });
|
|
260
|
-
`;
|
|
34
|
+
* Generate client.ts content
|
|
35
|
+
* @param options - Generation options
|
|
36
|
+
*/
|
|
37
|
+
export function generateClientFile(options = {}) {
|
|
38
|
+
const { browserCompatible = true } = options;
|
|
39
|
+
const templateName = browserCompatible
|
|
40
|
+
? 'client.browser.ts'
|
|
41
|
+
: 'client.node.ts';
|
|
42
|
+
return readTemplateFile(templateName, 'GraphQL client configuration and execution');
|
|
261
43
|
}
|
|
@@ -40,7 +40,9 @@ export function generate(options) {
|
|
|
40
40
|
// 1. Generate client.ts
|
|
41
41
|
files.push({
|
|
42
42
|
path: 'client.ts',
|
|
43
|
-
content: generateClientFile(
|
|
43
|
+
content: generateClientFile({
|
|
44
|
+
browserCompatible: config.browserCompatible ?? true,
|
|
45
|
+
}),
|
|
44
46
|
});
|
|
45
47
|
// Collect table type names for import path resolution
|
|
46
48
|
const tableTypeNames = new Set(tables.map((t) => getTableNames(t).typeName));
|
|
@@ -11,17 +11,20 @@ export interface GeneratedClientFile {
|
|
|
11
11
|
/**
|
|
12
12
|
* Generate the main client.ts file (OrmClient class)
|
|
13
13
|
* This is the runtime client that handles GraphQL execution
|
|
14
|
+
*
|
|
15
|
+
* Reads from the templates directory for proper type checking.
|
|
14
16
|
*/
|
|
15
17
|
export declare function generateOrmClientFile(): GeneratedClientFile;
|
|
16
18
|
/**
|
|
17
19
|
* Generate the query-builder.ts file (runtime query builder)
|
|
18
20
|
*
|
|
19
|
-
* Reads from the
|
|
20
|
-
* which enables proper type checking and testability.
|
|
21
|
+
* Reads from the templates directory for proper type checking and testability.
|
|
21
22
|
*/
|
|
22
23
|
export declare function generateQueryBuilderFile(): GeneratedClientFile;
|
|
23
24
|
/**
|
|
24
25
|
* Generate the select-types.ts file
|
|
26
|
+
*
|
|
27
|
+
* Reads from the templates directory for proper type checking.
|
|
25
28
|
*/
|
|
26
29
|
export declare function generateSelectTypesFile(): GeneratedClientFile;
|
|
27
30
|
/**
|
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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) {
|