@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.
@@ -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
+ }