@newmo/graphql-codegen-fake-server-client 0.22.0 → 0.23.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.
- package/dist/graphql-codegen-fake-server-client.js +89 -207
- package/dist/runtime/fake-client-runtime.js +141 -0
- package/dist/runtime-template.js +175 -0
- package/dist/template-helpers.js +291 -0
- package/dist/templates/method-generators.js +294 -0
- package/dist/templates/runtime.js +175 -0
- package/package.json +2 -2
- package/src/graphql-codegen-fake-server-client.ts +192 -276
- package/src/templates/method-generators.ts +324 -0
- package/src/templates/runtime.ts +172 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to indent each line of a string
|
|
3
|
+
*/
|
|
4
|
+
function indent(str: string, spaces = 4): string {
|
|
5
|
+
const indentStr = " ".repeat(spaces);
|
|
6
|
+
return str
|
|
7
|
+
.split("\n")
|
|
8
|
+
.map((line) => (line ? indentStr + line : line))
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create method templates with better readability
|
|
14
|
+
*/
|
|
15
|
+
function createMethod(config: {
|
|
16
|
+
name: string;
|
|
17
|
+
params: string;
|
|
18
|
+
returnType: string;
|
|
19
|
+
body: string;
|
|
20
|
+
}): string {
|
|
21
|
+
return `async ${config.name}(${config.params}): Promise<${config.returnType}> {
|
|
22
|
+
${indent(config.body)}
|
|
23
|
+
}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate register query method template
|
|
28
|
+
*/
|
|
29
|
+
export function generateRegisterQuery(params: {
|
|
30
|
+
name: string;
|
|
31
|
+
variablesType: string;
|
|
32
|
+
responseType: string;
|
|
33
|
+
endpoint: string;
|
|
34
|
+
}): string {
|
|
35
|
+
return createMethod({
|
|
36
|
+
name: `register${params.name}QueryResponse`,
|
|
37
|
+
params: `sequenceId:string, queryResponse: ${params.name}Query, sequenceOptions?: FakeClientRegisterSequenceOptions<${params.variablesType}>`,
|
|
38
|
+
returnType: params.responseType,
|
|
39
|
+
body: `const requestCondition = sequenceOptions?.requestCondition ?? { type: "always" };
|
|
40
|
+
const response = await requestQueue.add(() => fetchWithRetry(
|
|
41
|
+
${params.endpoint},
|
|
42
|
+
{
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'sequence-id': sequenceId
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
type: "operation",
|
|
50
|
+
operationName: "${params.name}",
|
|
51
|
+
data: queryResponse,
|
|
52
|
+
requestCondition: requestCondition
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
55
|
+
));
|
|
56
|
+
|
|
57
|
+
const result = await response.json();
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const errorResult = result as { errors?: string[] };
|
|
60
|
+
throw new Error(\`Failed to register fake response: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
61
|
+
}
|
|
62
|
+
return result as ${params.responseType};`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate register query error method template
|
|
68
|
+
*/
|
|
69
|
+
export function generateRegisterQueryError(params: {
|
|
70
|
+
name: string;
|
|
71
|
+
responseType: string;
|
|
72
|
+
endpoint: string;
|
|
73
|
+
}): string {
|
|
74
|
+
return createMethod({
|
|
75
|
+
name: `register${params.name}QueryErrorResponse`,
|
|
76
|
+
params: "sequenceId:string, { errors, responseStatusCode }: { errors: Record<string, unknown>[]; responseStatusCode: number }",
|
|
77
|
+
returnType: params.responseType,
|
|
78
|
+
body: `const response = await requestQueue.add(() => fetchWithRetry(
|
|
79
|
+
${params.endpoint},
|
|
80
|
+
{
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'sequence-id': sequenceId
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
type: "network-error",
|
|
88
|
+
operationName: "${params.name}",
|
|
89
|
+
responseStatusCode,
|
|
90
|
+
errors
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
));
|
|
94
|
+
|
|
95
|
+
const result = await response.json();
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const errorResult = result as { errors?: string[] };
|
|
98
|
+
throw new Error(\`Failed to register fake error response: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
99
|
+
}
|
|
100
|
+
return result as ${params.responseType};`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate register mutation method template
|
|
106
|
+
*/
|
|
107
|
+
export function generateRegisterMutation(params: {
|
|
108
|
+
name: string;
|
|
109
|
+
variablesType: string;
|
|
110
|
+
responseType: string;
|
|
111
|
+
endpoint: string;
|
|
112
|
+
}): string {
|
|
113
|
+
return createMethod({
|
|
114
|
+
name: `register${params.name}MutationResponse`,
|
|
115
|
+
params: `sequenceId:string, mutationResponse: ${params.name}Mutation, sequenceOptions?: FakeClientRegisterSequenceOptions<${params.variablesType}>`,
|
|
116
|
+
returnType: params.responseType,
|
|
117
|
+
body: `const requestCondition = sequenceOptions?.requestCondition ?? { type: "always" };
|
|
118
|
+
const response = await requestQueue.add(() => fetchWithRetry(
|
|
119
|
+
${params.endpoint},
|
|
120
|
+
{
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'sequence-id': sequenceId
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
type: "operation",
|
|
128
|
+
operationName: "${params.name}",
|
|
129
|
+
data: mutationResponse,
|
|
130
|
+
requestCondition: requestCondition
|
|
131
|
+
}),
|
|
132
|
+
}
|
|
133
|
+
));
|
|
134
|
+
|
|
135
|
+
const result = await response.json();
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const errorResult = result as { errors?: string[] };
|
|
138
|
+
throw new Error(\`Failed to register fake response: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
139
|
+
}
|
|
140
|
+
return result as ${params.responseType};`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate register mutation error method template
|
|
146
|
+
*/
|
|
147
|
+
export function generateRegisterMutationError(params: {
|
|
148
|
+
name: string;
|
|
149
|
+
responseType: string;
|
|
150
|
+
endpoint: string;
|
|
151
|
+
}): string {
|
|
152
|
+
return createMethod({
|
|
153
|
+
name: `register${params.name}MutationErrorResponse`,
|
|
154
|
+
params: "sequenceId:string, { errors, responseStatusCode }: { errors: Record<string, unknown>[]; responseStatusCode: number }",
|
|
155
|
+
returnType: params.responseType,
|
|
156
|
+
body: `const response = await requestQueue.add(() => fetchWithRetry(
|
|
157
|
+
${params.endpoint},
|
|
158
|
+
{
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
'sequence-id': sequenceId
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
type: "network-error",
|
|
166
|
+
operationName: "${params.name}",
|
|
167
|
+
responseStatusCode,
|
|
168
|
+
errors
|
|
169
|
+
}),
|
|
170
|
+
}
|
|
171
|
+
));
|
|
172
|
+
|
|
173
|
+
const result = await response.json();
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const errorResult = result as { errors?: string[] };
|
|
176
|
+
throw new Error(\`Failed to register fake error response: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
177
|
+
}
|
|
178
|
+
return result as ${params.responseType};`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate called query method template
|
|
184
|
+
*/
|
|
185
|
+
export function generateCalledQuery(params: {
|
|
186
|
+
name: string;
|
|
187
|
+
variablesType: string;
|
|
188
|
+
endpoint: string;
|
|
189
|
+
}): string {
|
|
190
|
+
return createMethod({
|
|
191
|
+
name: `called${params.name}Query`,
|
|
192
|
+
params: "sequenceId:string",
|
|
193
|
+
returnType: `{
|
|
194
|
+
ok: true;
|
|
195
|
+
data: {
|
|
196
|
+
requestTimestamp: number;
|
|
197
|
+
request: {
|
|
198
|
+
headers: Record<string, unknown>;
|
|
199
|
+
body: {
|
|
200
|
+
operationName: string;
|
|
201
|
+
query: string;
|
|
202
|
+
variables: ${params.variablesType};
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
response: {
|
|
206
|
+
statusCode: number;
|
|
207
|
+
headers: Record<string, unknown>;
|
|
208
|
+
body: ${params.name}Query;
|
|
209
|
+
};
|
|
210
|
+
}[]
|
|
211
|
+
}`,
|
|
212
|
+
body: `const response = await requestQueue.add(() => fetchWithRetry(
|
|
213
|
+
${params.endpoint},
|
|
214
|
+
{
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
'Content-Type': 'application/json',
|
|
218
|
+
'sequence-id': sequenceId
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
operationName: "${params.name}"
|
|
222
|
+
}),
|
|
223
|
+
}
|
|
224
|
+
));
|
|
225
|
+
|
|
226
|
+
const result = await response.json();
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const errorResult = result as { errors?: string[] };
|
|
229
|
+
throw new Error(\`Failed to get called data: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result as {
|
|
233
|
+
ok: true;
|
|
234
|
+
data: {
|
|
235
|
+
requestTimestamp: number;
|
|
236
|
+
request: {
|
|
237
|
+
headers: Record<string, unknown>;
|
|
238
|
+
body: {
|
|
239
|
+
operationName: string;
|
|
240
|
+
query: string;
|
|
241
|
+
variables: ${params.variablesType};
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
response: {
|
|
245
|
+
statusCode: number;
|
|
246
|
+
headers: Record<string, unknown>;
|
|
247
|
+
body: ${params.name}Query;
|
|
248
|
+
};
|
|
249
|
+
}[];
|
|
250
|
+
};`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Generate called mutation method template
|
|
256
|
+
*/
|
|
257
|
+
export function generateCalledMutation(params: {
|
|
258
|
+
name: string;
|
|
259
|
+
variablesType: string;
|
|
260
|
+
endpoint: string;
|
|
261
|
+
}): string {
|
|
262
|
+
return createMethod({
|
|
263
|
+
name: `called${params.name}Mutation`,
|
|
264
|
+
params: "sequenceId:string",
|
|
265
|
+
returnType: `{
|
|
266
|
+
ok: true;
|
|
267
|
+
data: {
|
|
268
|
+
requestTimestamp: number;
|
|
269
|
+
request: {
|
|
270
|
+
headers: Record<string, unknown>;
|
|
271
|
+
body: {
|
|
272
|
+
operationName: string;
|
|
273
|
+
query: string;
|
|
274
|
+
variables: ${params.variablesType};
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
response: {
|
|
278
|
+
statusCode: number;
|
|
279
|
+
headers: Record<string, unknown>;
|
|
280
|
+
body: ${params.name}Mutation;
|
|
281
|
+
};
|
|
282
|
+
}[];
|
|
283
|
+
}`,
|
|
284
|
+
body: `const response = await requestQueue.add(() => fetchWithRetry(
|
|
285
|
+
${params.endpoint},
|
|
286
|
+
{
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'sequence-id': sequenceId
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({
|
|
293
|
+
operationName: "${params.name}"
|
|
294
|
+
}),
|
|
295
|
+
}
|
|
296
|
+
));
|
|
297
|
+
|
|
298
|
+
const result = await response.json();
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const errorResult = result as { errors?: string[] };
|
|
301
|
+
throw new Error(\`Failed to get called data: \${response.status} \${response.statusText}\${errorResult.errors ? ' - ' + JSON.stringify(errorResult.errors) : ''}\`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result as {
|
|
305
|
+
ok: true;
|
|
306
|
+
data: {
|
|
307
|
+
requestTimestamp: number;
|
|
308
|
+
request: {
|
|
309
|
+
headers: Record<string, unknown>;
|
|
310
|
+
body: {
|
|
311
|
+
operationName: string;
|
|
312
|
+
query: string;
|
|
313
|
+
variables: ${params.variablesType};
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
response: {
|
|
317
|
+
statusCode: number;
|
|
318
|
+
headers: Record<string, unknown>;
|
|
319
|
+
body: ${params.name}Mutation;
|
|
320
|
+
};
|
|
321
|
+
}[];
|
|
322
|
+
};`,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the runtime code as a string template
|
|
3
|
+
* This is used to embed the runtime code in the generated client
|
|
4
|
+
*/
|
|
5
|
+
export function getRuntimeCode(): string {
|
|
6
|
+
return `// Runtime utilities for generated fake client
|
|
7
|
+
export type CreateFakeClientOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The URL of the fake server
|
|
10
|
+
* @example 'http://localhost:4000/fake'
|
|
11
|
+
*/
|
|
12
|
+
fakeServerEndpoint: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Request queue implementation for rate limiting
|
|
16
|
+
class RequestQueue {
|
|
17
|
+
private queue: Array<() => Promise<any>> = [];
|
|
18
|
+
private running = 0;
|
|
19
|
+
private maxConcurrent: number = 10; // Reduced default for better stability
|
|
20
|
+
private requestDelay: number = 10; // Small delay to prevent overwhelming the server
|
|
21
|
+
private lastRequestTime = 0;
|
|
22
|
+
|
|
23
|
+
async add<T>(fn: () => Promise<T>): Promise<T> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
this.queue.push(async () => {
|
|
26
|
+
try {
|
|
27
|
+
// Apply request delay if configured
|
|
28
|
+
if (this.requestDelay > 0) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
31
|
+
if (timeSinceLastRequest < this.requestDelay) {
|
|
32
|
+
await new Promise(r => setTimeout(r, this.requestDelay - timeSinceLastRequest));
|
|
33
|
+
}
|
|
34
|
+
this.lastRequestTime = Date.now();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await fn();
|
|
38
|
+
resolve(result);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.process();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async process() {
|
|
48
|
+
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.running++;
|
|
53
|
+
const fn = this.queue.shift();
|
|
54
|
+
if (fn) {
|
|
55
|
+
await fn();
|
|
56
|
+
this.running--;
|
|
57
|
+
this.process();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Retry helper function with exponential backoff
|
|
63
|
+
async function fetchWithRetry(
|
|
64
|
+
url: string,
|
|
65
|
+
options: RequestInit
|
|
66
|
+
): Promise<Response> {
|
|
67
|
+
const maxAttempts = 3;
|
|
68
|
+
const initialDelay = 100;
|
|
69
|
+
const maxDelay = 2000;
|
|
70
|
+
const backoffFactor = 2;
|
|
71
|
+
|
|
72
|
+
// Apply HTTP options with sensible defaults
|
|
73
|
+
const fetchOptions: RequestInit = {
|
|
74
|
+
...options,
|
|
75
|
+
// Enable keepalive for connection reuse
|
|
76
|
+
keepalive: true,
|
|
77
|
+
// Set a reasonable timeout (30 seconds)
|
|
78
|
+
signal: AbortSignal.timeout(30000),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let lastError: Error | undefined;
|
|
82
|
+
let lastResponse: Response | undefined;
|
|
83
|
+
|
|
84
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(url, fetchOptions);
|
|
87
|
+
lastResponse = response;
|
|
88
|
+
|
|
89
|
+
// Success (2xx) or client error (4xx) - don't retry
|
|
90
|
+
if (response.status < 500) {
|
|
91
|
+
return response;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Server error (5xx) - should retry
|
|
95
|
+
if (attempt < maxAttempts - 1) {
|
|
96
|
+
const requestInfo = {
|
|
97
|
+
url,
|
|
98
|
+
status: response.status,
|
|
99
|
+
statusText: response.statusText,
|
|
100
|
+
attempt: attempt + 1,
|
|
101
|
+
maxAttempts,
|
|
102
|
+
operationName: JSON.parse(options.body as string)?.operationName,
|
|
103
|
+
sequenceId: (options.headers as any)?.['sequence-id'],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
console.error(\`[FakeClient] Server error, will retry:\`, requestInfo);
|
|
107
|
+
|
|
108
|
+
// Calculate delay with exponential backoff and jitter
|
|
109
|
+
const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
|
110
|
+
const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
|
|
111
|
+
const delay = baseDelay + jitter;
|
|
112
|
+
|
|
113
|
+
console.log(\`[FakeClient] Retrying in \${Math.round(delay)}ms...\`);
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Last attempt and still server error
|
|
119
|
+
return response;
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
lastError = error as Error;
|
|
123
|
+
|
|
124
|
+
// Determine if error is retryable
|
|
125
|
+
let shouldRetry = false;
|
|
126
|
+
let errorType = 'unknown';
|
|
127
|
+
|
|
128
|
+
if (error instanceof TypeError) {
|
|
129
|
+
// Network errors from fetch (connection failures)
|
|
130
|
+
shouldRetry = true;
|
|
131
|
+
errorType = 'network';
|
|
132
|
+
} else if (error instanceof Error && error.name === 'AbortError') {
|
|
133
|
+
// Timeout errors
|
|
134
|
+
shouldRetry = true;
|
|
135
|
+
errorType = 'timeout';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const requestInfo = {
|
|
139
|
+
url,
|
|
140
|
+
errorType,
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
attempt: attempt + 1,
|
|
143
|
+
maxAttempts,
|
|
144
|
+
operationName: JSON.parse(options.body as string)?.operationName,
|
|
145
|
+
sequenceId: (options.headers as any)?.['sequence-id'],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
console.error(\`[FakeClient] Request failed:\`, requestInfo);
|
|
149
|
+
|
|
150
|
+
if (shouldRetry && attempt < maxAttempts - 1) {
|
|
151
|
+
// Calculate delay with exponential backoff and jitter
|
|
152
|
+
const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
|
153
|
+
const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
|
|
154
|
+
const delay = baseDelay + jitter;
|
|
155
|
+
|
|
156
|
+
console.log(\`[FakeClient] Retrying in \${Math.round(delay)}ms...\`);
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Not retryable or max attempts reached
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Should not reach here, but just in case
|
|
167
|
+
if (lastResponse) {
|
|
168
|
+
return lastResponse;
|
|
169
|
+
}
|
|
170
|
+
throw new Error('Max retry attempts reached', { cause: lastError });
|
|
171
|
+
}`;
|
|
172
|
+
}
|