@constructive-io/graphql-codegen 3.1.1 → 3.1.2
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/core/codegen/templates/client.node.ts +67 -86
- package/core/introspect/fetch-schema.js +61 -70
- package/core/output/writer.d.ts +5 -5
- package/core/output/writer.js +82 -20
- package/esm/core/introspect/fetch-schema.js +61 -70
- package/esm/core/output/writer.d.ts +5 -5
- package/esm/core/output/writer.js +82 -20
- package/package.json +2 -2
|
@@ -1,86 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GraphQL client configuration and execution (Node.js with
|
|
2
|
+
* GraphQL client configuration and execution (Node.js with native http/https)
|
|
3
3
|
*
|
|
4
4
|
* This is the RUNTIME code that gets copied to generated output.
|
|
5
|
-
* Uses
|
|
5
|
+
* Uses native Node.js http/https modules.
|
|
6
6
|
*
|
|
7
7
|
* NOTE: This file is read at codegen time and written to output.
|
|
8
8
|
* Any changes here will affect all generated clients.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import http from 'node:http';
|
|
12
|
+
import https from 'node:https';
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
|
-
//
|
|
15
|
+
// HTTP Request Helper
|
|
16
16
|
// ============================================================================
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return hostname === 'localhost' || hostname.endsWith('.localhost');
|
|
18
|
+
interface HttpResponse {
|
|
19
|
+
statusCode: number;
|
|
20
|
+
statusMessage: string;
|
|
21
|
+
data: string;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
27
|
-
* This fixes DNS resolution issues on macOS where subdomains like api.localhost
|
|
28
|
-
* don't resolve automatically (unlike browsers which handle *.localhost).
|
|
25
|
+
* Make an HTTP/HTTPS request using native Node modules
|
|
29
26
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
function makeRequest(
|
|
28
|
+
url: URL,
|
|
29
|
+
options: http.RequestOptions,
|
|
30
|
+
body: string
|
|
31
|
+
): Promise<HttpResponse> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
34
|
+
|
|
35
|
+
const req = protocol.request(url, options, (res) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
res.setEncoding('utf8');
|
|
38
|
+
res.on('data', (chunk: string) => {
|
|
39
|
+
data += chunk;
|
|
40
|
+
});
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
resolve({
|
|
43
|
+
statusCode: res.statusCode || 0,
|
|
44
|
+
statusMessage: res.statusMessage || '',
|
|
45
|
+
data,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
req.on('error', reject);
|
|
51
|
+
req.write(body);
|
|
52
|
+
req.end();
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
|
|
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
56
|
// ============================================================================
|
|
85
57
|
// Configuration
|
|
86
58
|
// ============================================================================
|
|
@@ -174,7 +146,7 @@ export class GraphQLClientError extends Error {
|
|
|
174
146
|
constructor(
|
|
175
147
|
message: string,
|
|
176
148
|
public errors: GraphQLError[],
|
|
177
|
-
public
|
|
149
|
+
public statusCode?: number
|
|
178
150
|
) {
|
|
179
151
|
super(message);
|
|
180
152
|
this.name = 'GraphQLClientError';
|
|
@@ -188,8 +160,6 @@ export class GraphQLClientError extends Error {
|
|
|
188
160
|
export interface ExecuteOptions {
|
|
189
161
|
/** Override headers for this request */
|
|
190
162
|
headers?: Record<string, string>;
|
|
191
|
-
/** AbortSignal for request cancellation */
|
|
192
|
-
signal?: AbortSignal;
|
|
193
163
|
}
|
|
194
164
|
|
|
195
165
|
/**
|
|
@@ -212,25 +182,29 @@ export async function execute<
|
|
|
212
182
|
options?: ExecuteOptions
|
|
213
183
|
): Promise<TData> {
|
|
214
184
|
const config = getConfig();
|
|
185
|
+
const url = new URL(config.endpoint);
|
|
186
|
+
|
|
187
|
+
const body = JSON.stringify({
|
|
188
|
+
query: document,
|
|
189
|
+
variables,
|
|
190
|
+
});
|
|
215
191
|
|
|
216
|
-
const
|
|
192
|
+
const requestOptions: http.RequestOptions = {
|
|
217
193
|
method: 'POST',
|
|
218
194
|
headers: {
|
|
219
195
|
'Content-Type': 'application/json',
|
|
220
196
|
...config.headers,
|
|
221
197
|
...options?.headers,
|
|
222
198
|
},
|
|
223
|
-
body: JSON.stringify({
|
|
224
|
-
query: document,
|
|
225
|
-
variables,
|
|
226
|
-
}),
|
|
227
|
-
signal: options?.signal,
|
|
228
199
|
};
|
|
229
200
|
|
|
230
|
-
const
|
|
231
|
-
|
|
201
|
+
const response = await makeRequest(url, requestOptions, body);
|
|
202
|
+
|
|
203
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
204
|
+
throw new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);
|
|
205
|
+
}
|
|
232
206
|
|
|
233
|
-
const json = (
|
|
207
|
+
const json = JSON.parse(response.data) as {
|
|
234
208
|
data?: TData;
|
|
235
209
|
errors?: GraphQLError[];
|
|
236
210
|
};
|
|
@@ -239,7 +213,7 @@ export async function execute<
|
|
|
239
213
|
throw new GraphQLClientError(
|
|
240
214
|
json.errors[0].message || 'GraphQL request failed',
|
|
241
215
|
json.errors,
|
|
242
|
-
response
|
|
216
|
+
response.statusCode
|
|
243
217
|
);
|
|
244
218
|
}
|
|
245
219
|
|
|
@@ -259,25 +233,32 @@ export async function executeWithErrors<
|
|
|
259
233
|
options?: ExecuteOptions
|
|
260
234
|
): Promise<{ data: TData | null; errors: GraphQLError[] | null }> {
|
|
261
235
|
const config = getConfig();
|
|
236
|
+
const url = new URL(config.endpoint);
|
|
262
237
|
|
|
263
|
-
const
|
|
238
|
+
const body = JSON.stringify({
|
|
239
|
+
query: document,
|
|
240
|
+
variables,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const requestOptions: http.RequestOptions = {
|
|
264
244
|
method: 'POST',
|
|
265
245
|
headers: {
|
|
266
246
|
'Content-Type': 'application/json',
|
|
267
247
|
...config.headers,
|
|
268
248
|
...options?.headers,
|
|
269
249
|
},
|
|
270
|
-
body: JSON.stringify({
|
|
271
|
-
query: document,
|
|
272
|
-
variables,
|
|
273
|
-
}),
|
|
274
|
-
signal: options?.signal,
|
|
275
250
|
};
|
|
276
251
|
|
|
277
|
-
const
|
|
278
|
-
|
|
252
|
+
const response = await makeRequest(url, requestOptions, body);
|
|
253
|
+
|
|
254
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
255
|
+
return {
|
|
256
|
+
data: null,
|
|
257
|
+
errors: [{ message: `HTTP ${response.statusCode}: ${response.statusMessage}` }],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
279
260
|
|
|
280
|
-
const json = (
|
|
261
|
+
const json = JSON.parse(response.data) as {
|
|
281
262
|
data?: TData;
|
|
282
263
|
errors?: GraphQLError[];
|
|
283
264
|
};
|
|
@@ -6,129 +6,120 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.fetchSchema = fetchSchema;
|
|
7
7
|
/**
|
|
8
8
|
* Fetch GraphQL schema introspection from an endpoint
|
|
9
|
+
* Uses native Node.js http/https modules
|
|
9
10
|
*/
|
|
10
|
-
const
|
|
11
|
-
const
|
|
11
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
12
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
12
13
|
const schema_query_1 = require("./schema-query");
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Make an HTTP/HTTPS request using native Node modules
|
|
15
16
|
*/
|
|
16
|
-
function
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
},
|
|
17
|
+
function makeRequest(url, options, body, timeout) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const protocol = url.protocol === 'https:' ? node_https_1.default : node_http_1.default;
|
|
20
|
+
const req = protocol.request(url, options, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
res.setEncoding('utf8');
|
|
23
|
+
res.on('data', (chunk) => {
|
|
24
|
+
data += chunk;
|
|
25
|
+
});
|
|
26
|
+
res.on('end', () => {
|
|
27
|
+
resolve({
|
|
28
|
+
statusCode: res.statusCode || 0,
|
|
29
|
+
statusMessage: res.statusMessage || '',
|
|
30
|
+
data,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
req.on('error', reject);
|
|
35
|
+
req.setTimeout(timeout, () => {
|
|
36
|
+
req.destroy();
|
|
37
|
+
reject(new Error(`Request timeout after ${timeout}ms`));
|
|
38
|
+
});
|
|
39
|
+
req.write(body);
|
|
40
|
+
req.end();
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
|
-
let localhostAgent = null;
|
|
45
|
-
function getLocalhostAgent() {
|
|
46
|
-
if (!localhostAgent) {
|
|
47
|
-
localhostAgent = createLocalhostAgent();
|
|
48
|
-
}
|
|
49
|
-
return localhostAgent;
|
|
50
|
-
}
|
|
51
43
|
/**
|
|
52
44
|
* Fetch the full schema introspection from a GraphQL endpoint
|
|
53
45
|
*/
|
|
54
46
|
async function fetchSchema(options) {
|
|
55
47
|
const { endpoint, authorization, headers = {}, timeout = 30000 } = options;
|
|
56
|
-
// Parse the endpoint URL to check for localhost
|
|
57
48
|
const url = new URL(endpoint);
|
|
58
|
-
const useLocalhostAgent = isLocalhostHostname(url.hostname);
|
|
59
|
-
// Build headers
|
|
60
49
|
const requestHeaders = {
|
|
61
50
|
'Content-Type': 'application/json',
|
|
62
51
|
Accept: 'application/json',
|
|
63
52
|
...headers,
|
|
64
53
|
};
|
|
65
|
-
// Set Host header for localhost subdomains to preserve routing
|
|
66
|
-
if (useLocalhostAgent && url.hostname !== 'localhost') {
|
|
67
|
-
requestHeaders['Host'] = url.hostname;
|
|
68
|
-
}
|
|
69
54
|
if (authorization) {
|
|
70
55
|
requestHeaders['Authorization'] = authorization;
|
|
71
56
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
57
|
+
const body = JSON.stringify({
|
|
58
|
+
query: schema_query_1.SCHEMA_INTROSPECTION_QUERY,
|
|
59
|
+
variables: {},
|
|
60
|
+
});
|
|
61
|
+
const requestOptions = {
|
|
77
62
|
method: 'POST',
|
|
78
63
|
headers: requestHeaders,
|
|
79
|
-
body: JSON.stringify({
|
|
80
|
-
query: schema_query_1.SCHEMA_INTROSPECTION_QUERY,
|
|
81
|
-
variables: {},
|
|
82
|
-
}),
|
|
83
|
-
signal: controller.signal,
|
|
84
64
|
};
|
|
85
|
-
// Use custom agent for localhost to fix DNS resolution on macOS
|
|
86
|
-
if (useLocalhostAgent) {
|
|
87
|
-
fetchOptions.dispatcher = getLocalhostAgent();
|
|
88
|
-
}
|
|
89
65
|
try {
|
|
90
|
-
const response = await (
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
66
|
+
const response = await makeRequest(url, requestOptions, body, timeout);
|
|
67
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
93
68
|
return {
|
|
94
69
|
success: false,
|
|
95
|
-
error: `HTTP ${response.
|
|
96
|
-
statusCode: response.
|
|
70
|
+
error: `HTTP ${response.statusCode}: ${response.statusMessage}`,
|
|
71
|
+
statusCode: response.statusCode,
|
|
97
72
|
};
|
|
98
73
|
}
|
|
99
|
-
const json = (
|
|
100
|
-
// Check for GraphQL errors
|
|
74
|
+
const json = JSON.parse(response.data);
|
|
101
75
|
if (json.errors && json.errors.length > 0) {
|
|
102
76
|
const errorMessages = json.errors.map((e) => e.message).join('; ');
|
|
103
77
|
return {
|
|
104
78
|
success: false,
|
|
105
79
|
error: `GraphQL errors: ${errorMessages}`,
|
|
106
|
-
statusCode: response.
|
|
80
|
+
statusCode: response.statusCode,
|
|
107
81
|
};
|
|
108
82
|
}
|
|
109
|
-
// Check if __schema is present
|
|
110
83
|
if (!json.data?.__schema) {
|
|
111
84
|
return {
|
|
112
85
|
success: false,
|
|
113
86
|
error: 'No __schema field in response. Introspection may be disabled on this endpoint.',
|
|
114
|
-
statusCode: response.
|
|
87
|
+
statusCode: response.statusCode,
|
|
115
88
|
};
|
|
116
89
|
}
|
|
117
90
|
return {
|
|
118
91
|
success: true,
|
|
119
92
|
data: json.data,
|
|
120
|
-
statusCode: response.
|
|
93
|
+
statusCode: response.statusCode,
|
|
121
94
|
};
|
|
122
95
|
}
|
|
123
96
|
catch (err) {
|
|
124
|
-
clearTimeout(timeoutId);
|
|
125
97
|
if (err instanceof Error) {
|
|
126
|
-
if (err.
|
|
98
|
+
if (err.message.includes('timeout')) {
|
|
127
99
|
return {
|
|
128
100
|
success: false,
|
|
129
101
|
error: `Request timeout after ${timeout}ms`,
|
|
130
102
|
};
|
|
131
103
|
}
|
|
104
|
+
const errorCode = err.code;
|
|
105
|
+
if (errorCode === 'ECONNREFUSED') {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: `Connection refused - is the server running at ${endpoint}?`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (errorCode === 'ENOTFOUND') {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: `DNS lookup failed for ${url.hostname} - check the endpoint URL`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (errorCode === 'ECONNRESET') {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: `Connection reset by server at ${endpoint}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
132
123
|
return {
|
|
133
124
|
success: false,
|
|
134
125
|
error: err.message,
|
package/core/output/writer.d.ts
CHANGED
|
@@ -27,12 +27,12 @@ export interface WriteOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function writeGeneratedFiles(files: GeneratedFile[], outputDir: string, subdirs: string[], options?: WriteOptions): Promise<WriteResult>;
|
|
29
29
|
/**
|
|
30
|
-
* Format generated files using oxfmt
|
|
30
|
+
* Format generated files using oxfmt programmatically
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* @deprecated Use writeGeneratedFiles with formatFiles option instead.
|
|
33
|
+
* This function is kept for backwards compatibility.
|
|
34
34
|
*/
|
|
35
|
-
export declare function formatOutput(outputDir: string): {
|
|
35
|
+
export declare function formatOutput(outputDir: string): Promise<{
|
|
36
36
|
success: boolean;
|
|
37
37
|
error?: string;
|
|
38
|
-
}
|
|
38
|
+
}>;
|
package/core/output/writer.js
CHANGED
|
@@ -43,7 +43,37 @@ exports.formatOutput = formatOutput;
|
|
|
43
43
|
*/
|
|
44
44
|
const fs = __importStar(require("node:fs"));
|
|
45
45
|
const path = __importStar(require("node:path"));
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Dynamically import oxfmt's format function
|
|
48
|
+
* Returns null if oxfmt is not available
|
|
49
|
+
*/
|
|
50
|
+
async function getOxfmtFormat() {
|
|
51
|
+
try {
|
|
52
|
+
const oxfmt = await Promise.resolve().then(() => __importStar(require('oxfmt')));
|
|
53
|
+
return oxfmt.format;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Format a single file's content using oxfmt programmatically
|
|
61
|
+
*/
|
|
62
|
+
async function formatFileContent(fileName, content, formatFn) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await formatFn(fileName, content, {
|
|
65
|
+
singleQuote: true,
|
|
66
|
+
trailingComma: 'es5',
|
|
67
|
+
tabWidth: 2,
|
|
68
|
+
semi: true,
|
|
69
|
+
});
|
|
70
|
+
return result.code;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// If formatting fails, return original content
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
47
77
|
/**
|
|
48
78
|
* Write generated files to disk
|
|
49
79
|
*
|
|
@@ -83,6 +113,11 @@ async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
|
|
|
83
113
|
if (errors.length > 0) {
|
|
84
114
|
return { success: false, errors };
|
|
85
115
|
}
|
|
116
|
+
// Get oxfmt format function if formatting is enabled
|
|
117
|
+
const formatFn = formatFiles ? await getOxfmtFormat() : null;
|
|
118
|
+
if (formatFiles && !formatFn && showProgress) {
|
|
119
|
+
console.warn('Warning: oxfmt not available, files will not be formatted');
|
|
120
|
+
}
|
|
86
121
|
for (let i = 0; i < files.length; i++) {
|
|
87
122
|
const file = files[i];
|
|
88
123
|
const filePath = path.join(outputDir, file.path);
|
|
@@ -105,8 +140,13 @@ async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
|
|
|
105
140
|
catch {
|
|
106
141
|
// Ignore if already exists
|
|
107
142
|
}
|
|
143
|
+
// Format content if oxfmt is available and file is TypeScript
|
|
144
|
+
let content = file.content;
|
|
145
|
+
if (formatFn && file.path.endsWith('.ts')) {
|
|
146
|
+
content = await formatFileContent(file.path, content, formatFn);
|
|
147
|
+
}
|
|
108
148
|
try {
|
|
109
|
-
fs.writeFileSync(filePath,
|
|
149
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
110
150
|
written.push(filePath);
|
|
111
151
|
}
|
|
112
152
|
catch (err) {
|
|
@@ -118,16 +158,6 @@ async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
|
|
|
118
158
|
if (showProgress && isTTY) {
|
|
119
159
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
120
160
|
}
|
|
121
|
-
// Format all generated files with oxfmt
|
|
122
|
-
if (formatFiles && errors.length === 0) {
|
|
123
|
-
if (showProgress) {
|
|
124
|
-
console.log('Formatting generated files...');
|
|
125
|
-
}
|
|
126
|
-
const formatResult = formatOutput(outputDir);
|
|
127
|
-
if (!formatResult.success && showProgress) {
|
|
128
|
-
console.warn('Warning: Failed to format generated files:', formatResult.error);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
161
|
return {
|
|
132
162
|
success: errors.length === 0,
|
|
133
163
|
filesWritten: written,
|
|
@@ -135,18 +165,50 @@ async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
|
|
|
135
165
|
};
|
|
136
166
|
}
|
|
137
167
|
/**
|
|
138
|
-
*
|
|
168
|
+
* Recursively find all .ts files in a directory
|
|
169
|
+
*/
|
|
170
|
+
function findTsFiles(dir) {
|
|
171
|
+
const files = [];
|
|
172
|
+
try {
|
|
173
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const fullPath = path.join(dir, entry.name);
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
files.push(...findTsFiles(fullPath));
|
|
178
|
+
}
|
|
179
|
+
else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
180
|
+
files.push(fullPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Ignore errors reading directories
|
|
186
|
+
}
|
|
187
|
+
return files;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Format generated files using oxfmt programmatically
|
|
139
191
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
192
|
+
* @deprecated Use writeGeneratedFiles with formatFiles option instead.
|
|
193
|
+
* This function is kept for backwards compatibility.
|
|
142
194
|
*/
|
|
143
|
-
function formatOutput(outputDir) {
|
|
195
|
+
async function formatOutput(outputDir) {
|
|
196
|
+
const formatFn = await getOxfmtFormat();
|
|
197
|
+
if (!formatFn) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
error: 'oxfmt not available. Install it with: npm install oxfmt',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
144
203
|
const absoluteOutputDir = path.resolve(outputDir);
|
|
145
204
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
// Find all .ts files in the output directory
|
|
206
|
+
const tsFiles = findTsFiles(absoluteOutputDir);
|
|
207
|
+
for (const filePath of tsFiles) {
|
|
208
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
209
|
+
const formatted = await formatFileContent(path.basename(filePath), content, formatFn);
|
|
210
|
+
fs.writeFileSync(filePath, formatted, 'utf-8');
|
|
211
|
+
}
|
|
150
212
|
return { success: true };
|
|
151
213
|
}
|
|
152
214
|
catch (err) {
|
|
@@ -1,128 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch GraphQL schema introspection from an endpoint
|
|
3
|
+
* Uses native Node.js http/https modules
|
|
3
4
|
*/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import https from 'node:https';
|
|
6
7
|
import { SCHEMA_INTROSPECTION_QUERY } from './schema-query';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Make an HTTP/HTTPS request using native Node modules
|
|
9
10
|
*/
|
|
10
|
-
function
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
},
|
|
11
|
+
function makeRequest(url, options, body, timeout) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
14
|
+
const req = protocol.request(url, options, (res) => {
|
|
15
|
+
let data = '';
|
|
16
|
+
res.setEncoding('utf8');
|
|
17
|
+
res.on('data', (chunk) => {
|
|
18
|
+
data += chunk;
|
|
19
|
+
});
|
|
20
|
+
res.on('end', () => {
|
|
21
|
+
resolve({
|
|
22
|
+
statusCode: res.statusCode || 0,
|
|
23
|
+
statusMessage: res.statusMessage || '',
|
|
24
|
+
data,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
req.setTimeout(timeout, () => {
|
|
30
|
+
req.destroy();
|
|
31
|
+
reject(new Error(`Request timeout after ${timeout}ms`));
|
|
32
|
+
});
|
|
33
|
+
req.write(body);
|
|
34
|
+
req.end();
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
|
-
let localhostAgent = null;
|
|
39
|
-
function getLocalhostAgent() {
|
|
40
|
-
if (!localhostAgent) {
|
|
41
|
-
localhostAgent = createLocalhostAgent();
|
|
42
|
-
}
|
|
43
|
-
return localhostAgent;
|
|
44
|
-
}
|
|
45
37
|
/**
|
|
46
38
|
* Fetch the full schema introspection from a GraphQL endpoint
|
|
47
39
|
*/
|
|
48
40
|
export async function fetchSchema(options) {
|
|
49
41
|
const { endpoint, authorization, headers = {}, timeout = 30000 } = options;
|
|
50
|
-
// Parse the endpoint URL to check for localhost
|
|
51
42
|
const url = new URL(endpoint);
|
|
52
|
-
const useLocalhostAgent = isLocalhostHostname(url.hostname);
|
|
53
|
-
// Build headers
|
|
54
43
|
const requestHeaders = {
|
|
55
44
|
'Content-Type': 'application/json',
|
|
56
45
|
Accept: 'application/json',
|
|
57
46
|
...headers,
|
|
58
47
|
};
|
|
59
|
-
// Set Host header for localhost subdomains to preserve routing
|
|
60
|
-
if (useLocalhostAgent && url.hostname !== 'localhost') {
|
|
61
|
-
requestHeaders['Host'] = url.hostname;
|
|
62
|
-
}
|
|
63
48
|
if (authorization) {
|
|
64
49
|
requestHeaders['Authorization'] = authorization;
|
|
65
50
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
51
|
+
const body = JSON.stringify({
|
|
52
|
+
query: SCHEMA_INTROSPECTION_QUERY,
|
|
53
|
+
variables: {},
|
|
54
|
+
});
|
|
55
|
+
const requestOptions = {
|
|
71
56
|
method: 'POST',
|
|
72
57
|
headers: requestHeaders,
|
|
73
|
-
body: JSON.stringify({
|
|
74
|
-
query: SCHEMA_INTROSPECTION_QUERY,
|
|
75
|
-
variables: {},
|
|
76
|
-
}),
|
|
77
|
-
signal: controller.signal,
|
|
78
58
|
};
|
|
79
|
-
// Use custom agent for localhost to fix DNS resolution on macOS
|
|
80
|
-
if (useLocalhostAgent) {
|
|
81
|
-
fetchOptions.dispatcher = getLocalhostAgent();
|
|
82
|
-
}
|
|
83
59
|
try {
|
|
84
|
-
const response = await
|
|
85
|
-
|
|
86
|
-
if (!response.ok) {
|
|
60
|
+
const response = await makeRequest(url, requestOptions, body, timeout);
|
|
61
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
87
62
|
return {
|
|
88
63
|
success: false,
|
|
89
|
-
error: `HTTP ${response.
|
|
90
|
-
statusCode: response.
|
|
64
|
+
error: `HTTP ${response.statusCode}: ${response.statusMessage}`,
|
|
65
|
+
statusCode: response.statusCode,
|
|
91
66
|
};
|
|
92
67
|
}
|
|
93
|
-
const json = (
|
|
94
|
-
// Check for GraphQL errors
|
|
68
|
+
const json = JSON.parse(response.data);
|
|
95
69
|
if (json.errors && json.errors.length > 0) {
|
|
96
70
|
const errorMessages = json.errors.map((e) => e.message).join('; ');
|
|
97
71
|
return {
|
|
98
72
|
success: false,
|
|
99
73
|
error: `GraphQL errors: ${errorMessages}`,
|
|
100
|
-
statusCode: response.
|
|
74
|
+
statusCode: response.statusCode,
|
|
101
75
|
};
|
|
102
76
|
}
|
|
103
|
-
// Check if __schema is present
|
|
104
77
|
if (!json.data?.__schema) {
|
|
105
78
|
return {
|
|
106
79
|
success: false,
|
|
107
80
|
error: 'No __schema field in response. Introspection may be disabled on this endpoint.',
|
|
108
|
-
statusCode: response.
|
|
81
|
+
statusCode: response.statusCode,
|
|
109
82
|
};
|
|
110
83
|
}
|
|
111
84
|
return {
|
|
112
85
|
success: true,
|
|
113
86
|
data: json.data,
|
|
114
|
-
statusCode: response.
|
|
87
|
+
statusCode: response.statusCode,
|
|
115
88
|
};
|
|
116
89
|
}
|
|
117
90
|
catch (err) {
|
|
118
|
-
clearTimeout(timeoutId);
|
|
119
91
|
if (err instanceof Error) {
|
|
120
|
-
if (err.
|
|
92
|
+
if (err.message.includes('timeout')) {
|
|
121
93
|
return {
|
|
122
94
|
success: false,
|
|
123
95
|
error: `Request timeout after ${timeout}ms`,
|
|
124
96
|
};
|
|
125
97
|
}
|
|
98
|
+
const errorCode = err.code;
|
|
99
|
+
if (errorCode === 'ECONNREFUSED') {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: `Connection refused - is the server running at ${endpoint}?`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (errorCode === 'ENOTFOUND') {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: `DNS lookup failed for ${url.hostname} - check the endpoint URL`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (errorCode === 'ECONNRESET') {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: `Connection reset by server at ${endpoint}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
126
117
|
return {
|
|
127
118
|
success: false,
|
|
128
119
|
error: err.message,
|
|
@@ -27,12 +27,12 @@ export interface WriteOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function writeGeneratedFiles(files: GeneratedFile[], outputDir: string, subdirs: string[], options?: WriteOptions): Promise<WriteResult>;
|
|
29
29
|
/**
|
|
30
|
-
* Format generated files using oxfmt
|
|
30
|
+
* Format generated files using oxfmt programmatically
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* @deprecated Use writeGeneratedFiles with formatFiles option instead.
|
|
33
|
+
* This function is kept for backwards compatibility.
|
|
34
34
|
*/
|
|
35
|
-
export declare function formatOutput(outputDir: string): {
|
|
35
|
+
export declare function formatOutput(outputDir: string): Promise<{
|
|
36
36
|
success: boolean;
|
|
37
37
|
error?: string;
|
|
38
|
-
}
|
|
38
|
+
}>;
|
|
@@ -6,7 +6,37 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Dynamically import oxfmt's format function
|
|
11
|
+
* Returns null if oxfmt is not available
|
|
12
|
+
*/
|
|
13
|
+
async function getOxfmtFormat() {
|
|
14
|
+
try {
|
|
15
|
+
const oxfmt = await import('oxfmt');
|
|
16
|
+
return oxfmt.format;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format a single file's content using oxfmt programmatically
|
|
24
|
+
*/
|
|
25
|
+
async function formatFileContent(fileName, content, formatFn) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await formatFn(fileName, content, {
|
|
28
|
+
singleQuote: true,
|
|
29
|
+
trailingComma: 'es5',
|
|
30
|
+
tabWidth: 2,
|
|
31
|
+
semi: true,
|
|
32
|
+
});
|
|
33
|
+
return result.code;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// If formatting fails, return original content
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
10
40
|
/**
|
|
11
41
|
* Write generated files to disk
|
|
12
42
|
*
|
|
@@ -46,6 +76,11 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
|
|
|
46
76
|
if (errors.length > 0) {
|
|
47
77
|
return { success: false, errors };
|
|
48
78
|
}
|
|
79
|
+
// Get oxfmt format function if formatting is enabled
|
|
80
|
+
const formatFn = formatFiles ? await getOxfmtFormat() : null;
|
|
81
|
+
if (formatFiles && !formatFn && showProgress) {
|
|
82
|
+
console.warn('Warning: oxfmt not available, files will not be formatted');
|
|
83
|
+
}
|
|
49
84
|
for (let i = 0; i < files.length; i++) {
|
|
50
85
|
const file = files[i];
|
|
51
86
|
const filePath = path.join(outputDir, file.path);
|
|
@@ -68,8 +103,13 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
|
|
|
68
103
|
catch {
|
|
69
104
|
// Ignore if already exists
|
|
70
105
|
}
|
|
106
|
+
// Format content if oxfmt is available and file is TypeScript
|
|
107
|
+
let content = file.content;
|
|
108
|
+
if (formatFn && file.path.endsWith('.ts')) {
|
|
109
|
+
content = await formatFileContent(file.path, content, formatFn);
|
|
110
|
+
}
|
|
71
111
|
try {
|
|
72
|
-
fs.writeFileSync(filePath,
|
|
112
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
73
113
|
written.push(filePath);
|
|
74
114
|
}
|
|
75
115
|
catch (err) {
|
|
@@ -81,16 +121,6 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
|
|
|
81
121
|
if (showProgress && isTTY) {
|
|
82
122
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
83
123
|
}
|
|
84
|
-
// Format all generated files with oxfmt
|
|
85
|
-
if (formatFiles && errors.length === 0) {
|
|
86
|
-
if (showProgress) {
|
|
87
|
-
console.log('Formatting generated files...');
|
|
88
|
-
}
|
|
89
|
-
const formatResult = formatOutput(outputDir);
|
|
90
|
-
if (!formatResult.success && showProgress) {
|
|
91
|
-
console.warn('Warning: Failed to format generated files:', formatResult.error);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
124
|
return {
|
|
95
125
|
success: errors.length === 0,
|
|
96
126
|
filesWritten: written,
|
|
@@ -98,18 +128,50 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
|
|
|
98
128
|
};
|
|
99
129
|
}
|
|
100
130
|
/**
|
|
101
|
-
*
|
|
131
|
+
* Recursively find all .ts files in a directory
|
|
132
|
+
*/
|
|
133
|
+
function findTsFiles(dir) {
|
|
134
|
+
const files = [];
|
|
135
|
+
try {
|
|
136
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const fullPath = path.join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
files.push(...findTsFiles(fullPath));
|
|
141
|
+
}
|
|
142
|
+
else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
143
|
+
files.push(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Ignore errors reading directories
|
|
149
|
+
}
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Format generated files using oxfmt programmatically
|
|
102
154
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
155
|
+
* @deprecated Use writeGeneratedFiles with formatFiles option instead.
|
|
156
|
+
* This function is kept for backwards compatibility.
|
|
105
157
|
*/
|
|
106
|
-
export function formatOutput(outputDir) {
|
|
158
|
+
export async function formatOutput(outputDir) {
|
|
159
|
+
const formatFn = await getOxfmtFormat();
|
|
160
|
+
if (!formatFn) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: 'oxfmt not available. Install it with: npm install oxfmt',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
107
166
|
const absoluteOutputDir = path.resolve(outputDir);
|
|
108
167
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
168
|
+
// Find all .ts files in the output directory
|
|
169
|
+
const tsFiles = findTsFiles(absoluteOutputDir);
|
|
170
|
+
for (const filePath of tsFiles) {
|
|
171
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
172
|
+
const formatted = await formatFileContent(path.basename(filePath), content, formatFn);
|
|
173
|
+
fs.writeFileSync(filePath, formatted, 'utf-8');
|
|
174
|
+
}
|
|
113
175
|
return { success: true };
|
|
114
176
|
}
|
|
115
177
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "GraphQL SDK generator for Constructive databases with React Query hooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "96a9219694ed3a992f6d909867f777818979ee6b"
|
|
103
103
|
}
|