@apollo-deploy/tesseract 0.1.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/typescript/index.d.ts +11 -0
- package/dist/adapters/typescript/index.d.ts.map +1 -0
- package/dist/adapters/typescript/index.js +112 -0
- package/dist/adapters/typescript/index.js.map +1 -0
- package/dist/adapters/typescript/model-emitter.d.ts +13 -0
- package/dist/adapters/typescript/model-emitter.d.ts.map +1 -0
- package/dist/adapters/typescript/model-emitter.js +415 -0
- package/dist/adapters/typescript/model-emitter.js.map +1 -0
- package/dist/adapters/typescript/shared.d.ts +26 -0
- package/dist/adapters/typescript/shared.d.ts.map +1 -0
- package/dist/adapters/typescript/shared.js +65 -0
- package/dist/adapters/typescript/shared.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +53 -0
- package/dist/cli.js.map +1 -0
- package/dist/helpers/handlebars.d.ts +15 -0
- package/dist/helpers/handlebars.d.ts.map +1 -0
- package/dist/helpers/handlebars.js +172 -0
- package/dist/helpers/handlebars.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/enrich.d.ts +7 -0
- package/dist/pipeline/enrich.d.ts.map +1 -0
- package/dist/pipeline/enrich.js +277 -0
- package/dist/pipeline/enrich.js.map +1 -0
- package/dist/pipeline/intake.d.ts +11 -0
- package/dist/pipeline/intake.d.ts.map +1 -0
- package/dist/pipeline/intake.js +508 -0
- package/dist/pipeline/intake.js.map +1 -0
- package/dist/pipeline/write.d.ts +15 -0
- package/dist/pipeline/write.d.ts.map +1 -0
- package/dist/pipeline/write.js +33 -0
- package/dist/pipeline/write.js.map +1 -0
- package/dist/types/config.d.ts +47 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +24 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/ir.d.ts +172 -0
- package/dist/types/ir.d.ts.map +1 -0
- package/dist/types/ir.js +7 -0
- package/dist/types/ir.js.map +1 -0
- package/dist/types/manifest.d.ts +112 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +32 -0
- package/dist/types/manifest.js.map +1 -0
- package/dist/utils/format.d.ts +5 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +21 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/naming.d.ts +26 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +270 -0
- package/dist/utils/naming.js.map +1 -0
- package/package.json +58 -0
- package/templates/typescript/client.hbs +145 -0
- package/templates/typescript/domain-index.hbs +10 -0
- package/templates/typescript/domain.hbs +127 -0
- package/templates/typescript/index.hbs +64 -0
- package/templates/typescript/package-json.hbs +29 -0
- package/templates/typescript/readme.hbs +78 -0
- package/templates/typescript/transport-sse.hbs +377 -0
- package/templates/typescript/transport.hbs +662 -0
- package/templates/typescript/tsconfig.hbs +18 -0
- package/templates/typescript/types-common.hbs +98 -0
- package/templates/typescript/types-errors.hbs +191 -0
- package/templates/typescript/types.hbs +33 -0
- package/templates/typescript/utils-index.hbs +7 -0
- package/templates/typescript/utils-query.hbs +29 -0
- package/templates/typescript/webhook-handler.hbs +447 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated
|
|
3
|
+
* This file was automatically generated by sdk-forge.
|
|
4
|
+
* DO NOT MODIFY THIS FILE DIRECTLY.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
import axios, {
|
|
9
|
+
type AxiosError,
|
|
10
|
+
type AxiosInstance,
|
|
11
|
+
type AxiosRequestConfig,
|
|
12
|
+
type AxiosResponse,
|
|
13
|
+
type InternalAxiosRequestConfig,
|
|
14
|
+
} from 'axios';
|
|
15
|
+
import { SDKError } from '../types/errors.js';
|
|
16
|
+
import type { ApiErrorEnvelope } from '../types/common.js';
|
|
17
|
+
|
|
18
|
+
// Re-export for backward compatibility
|
|
19
|
+
export { SDKError } from '../types/errors.js';
|
|
20
|
+
|
|
21
|
+
// ── Retry Utilities ─────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface RetryConfig {
|
|
24
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
25
|
+
attempts: number;
|
|
26
|
+
/** Base backoff delay in milliseconds (default: 400) */
|
|
27
|
+
backoffMs: number;
|
|
28
|
+
/** Whether to apply jitter to the backoff delay (default: true) */
|
|
29
|
+
jitter: boolean;
|
|
30
|
+
/** Maximum backoff delay in milliseconds (default: 30000) */
|
|
31
|
+
maxBackoffMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Custom predicate to determine if a failed request should be retried.
|
|
34
|
+
* Receives the error and the current attempt number (0-indexed).
|
|
35
|
+
* Return `true` to retry, `false` to fail immediately.
|
|
36
|
+
* When not provided, retries on network errors, timeouts, 429, and 5xx.
|
|
37
|
+
*/
|
|
38
|
+
shouldRetry?: (error: SDKError, attempt: number) => boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Custom function to compute the retry delay in milliseconds.
|
|
41
|
+
* Receives the error, current attempt number, and the computed default delay.
|
|
42
|
+
* When not provided, uses exponential backoff with optional jitter.
|
|
43
|
+
*/
|
|
44
|
+
delay?: (error: SDKError, attempt: number, computedDelayMs: number) => number;
|
|
45
|
+
/**
|
|
46
|
+
* Called before each retry attempt. Useful for logging or metrics.
|
|
47
|
+
* Return `false` to abort retrying.
|
|
48
|
+
*/
|
|
49
|
+
onRetry?: (error: SDKError, attempt: number, delayMs: number) => void | false | Promise<void | false>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function calculateBackoff(attempt: number, baseMs: number, useJitter: boolean, maxMs: number): number {
|
|
53
|
+
const exponential = Math.min(baseMs * Math.pow(2, attempt), maxMs);
|
|
54
|
+
if (!useJitter) return exponential;
|
|
55
|
+
// Full jitter: uniform random between 0 and exponential cap
|
|
56
|
+
return Math.floor(Math.random() * (exponential + 1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseRetryAfterMs(error: unknown): number | undefined {
|
|
60
|
+
if (!isAxiosError(error)) return undefined;
|
|
61
|
+
|
|
62
|
+
const rawRetryAfter = error.response?.headers?.['retry-after'];
|
|
63
|
+
if (typeof rawRetryAfter !== 'string' || rawRetryAfter.length === 0) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try integer seconds first
|
|
68
|
+
const seconds = Number(rawRetryAfter);
|
|
69
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
70
|
+
return Math.ceil(seconds) * 1000;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try HTTP-date format
|
|
74
|
+
const absoluteTime = Date.parse(rawRetryAfter);
|
|
75
|
+
if (!Number.isNaN(absoluteTime)) {
|
|
76
|
+
return Math.max(absoluteTime - Date.now(), 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
if (signal?.aborted) {
|
|
85
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const timer = setTimeout(resolve, ms);
|
|
89
|
+
signal?.addEventListener('abort', () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
92
|
+
}, { once: true });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Plugin API ───────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extensibility hooks for transforming or observing every request and response.
|
|
100
|
+
* Register plugins via `TransportConfig.plugins`; they are applied in array order.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const loggingPlugin: SDKPlugin = {
|
|
105
|
+
* name: 'logger',
|
|
106
|
+
* beforeRequest(config) { console.log('→', config.method, config.url); },
|
|
107
|
+
* afterResponse(response) { console.log('←', response.status); },
|
|
108
|
+
* };
|
|
109
|
+
* const transport = createTransport({ baseUrl: '...', plugins: [loggingPlugin] });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export interface SDKPlugin {
|
|
113
|
+
/** Optional plugin name for debugging and identification */
|
|
114
|
+
name?: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Called before each request is dispatched to the network.
|
|
118
|
+
* Return a modified `InternalAxiosRequestConfig` to replace it, or `void` to leave it unchanged.
|
|
119
|
+
*/
|
|
120
|
+
beforeRequest?: (
|
|
121
|
+
config: InternalAxiosRequestConfig,
|
|
122
|
+
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig> | void | Promise<void>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Called after a successful response is received, before data reaches the caller.
|
|
126
|
+
* Return a modified `AxiosResponse` to replace it, or `void` to leave it unchanged.
|
|
127
|
+
*/
|
|
128
|
+
afterResponse?: (
|
|
129
|
+
response: AxiosResponse,
|
|
130
|
+
) => AxiosResponse | Promise<AxiosResponse> | void | Promise<void>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Called when a request fails (after retries are exhausted).
|
|
134
|
+
* Can transform the error or perform side effects like logging.
|
|
135
|
+
* Return a modified `SDKError` to replace it, or `void` to leave it unchanged.
|
|
136
|
+
*/
|
|
137
|
+
onError?: (
|
|
138
|
+
error: SDKError,
|
|
139
|
+
config: AxiosRequestConfig,
|
|
140
|
+
) => SDKError | Promise<SDKError> | void | Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Transport Config ────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface TransportConfig {
|
|
146
|
+
baseUrl: string;
|
|
147
|
+
{{#each securitySchemes}}
|
|
148
|
+
{{#if (eq type "apiKey")}}
|
|
149
|
+
/** API key value for the {{name}} security scheme */
|
|
150
|
+
{{configKey}}?: string;
|
|
151
|
+
{{/if}}
|
|
152
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
153
|
+
/** Bearer token for the {{name}} security scheme */
|
|
154
|
+
{{configKey}}?: string;
|
|
155
|
+
{{/if}}
|
|
156
|
+
{{#if (eq type "oauth2")}}
|
|
157
|
+
/** OAuth2 access token for the {{name}} security scheme */
|
|
158
|
+
{{configKey}}?: string;
|
|
159
|
+
{{/if}}
|
|
160
|
+
{{#if (eq type "openIdConnect")}}
|
|
161
|
+
/** OpenID Connect access token for the {{name}} security scheme */
|
|
162
|
+
{{configKey}}?: string;
|
|
163
|
+
{{/if}}
|
|
164
|
+
{{/each}}
|
|
165
|
+
/** Request timeout in milliseconds (default: 15000) */
|
|
166
|
+
timeoutMs?: number;
|
|
167
|
+
/** Retry configuration */
|
|
168
|
+
retries?: RetryConfig;
|
|
169
|
+
/** Custom idempotency key generator for mutating requests */
|
|
170
|
+
getIdempotencyKey?: () => string;
|
|
171
|
+
/** Called before each request is dispatched */
|
|
172
|
+
onRequest?: (meta: RequestMeta) => void;
|
|
173
|
+
/** Called after a successful response is received */
|
|
174
|
+
onResponse?: (meta: ResponseMeta) => void;
|
|
175
|
+
/** Called when a request fails */
|
|
176
|
+
onError?: (meta: ErrorMeta) => void;
|
|
177
|
+
/**
|
|
178
|
+
* Send credentials (cookies, HTTP auth) on cross-origin requests.
|
|
179
|
+
* Maps directly to Axios's `withCredentials` option — effective in browser environments.
|
|
180
|
+
* In Node.js use `cookies` or `cookieJar` instead.
|
|
181
|
+
*/
|
|
182
|
+
withCredentials?: boolean;
|
|
183
|
+
/** Static cookie key/value pairs sent with every request */
|
|
184
|
+
cookies?: Record<string, string>;
|
|
185
|
+
/**
|
|
186
|
+
* Ordered list of plugins applied to every request and response.
|
|
187
|
+
* See {@link SDKPlugin} for the available hooks.
|
|
188
|
+
*/
|
|
189
|
+
plugins?: SDKPlugin[];
|
|
190
|
+
/**
|
|
191
|
+
* Default headers sent with every request.
|
|
192
|
+
* Merged beneath per-request headers (per-request wins on conflict).
|
|
193
|
+
*/
|
|
194
|
+
defaultHeaders?: Record<string, string>;
|
|
195
|
+
/**
|
|
196
|
+
* Custom Axios instance configuration merged into the transport instance.
|
|
197
|
+
* Useful for setting proxy, httpsAgent, or other Axios-specific options.
|
|
198
|
+
*/
|
|
199
|
+
axiosConfig?: Omit<AxiosRequestConfig, 'baseURL' | 'timeout' | 'withCredentials' | 'headers'>;
|
|
200
|
+
/**
|
|
201
|
+
* Custom function to map raw errors to SDKError.
|
|
202
|
+
* When provided, this replaces the built-in error mapper entirely.
|
|
203
|
+
*/
|
|
204
|
+
mapError?: (error: unknown) => SDKError;
|
|
205
|
+
/**
|
|
206
|
+
* Custom function to extract response data from the Axios response.
|
|
207
|
+
* Useful when the API wraps responses in an envelope.
|
|
208
|
+
* When not provided, returns `response.data` directly.
|
|
209
|
+
*/
|
|
210
|
+
responseTransform?: <T>(response: AxiosResponse) => T;
|
|
211
|
+
/**
|
|
212
|
+
* HTTP methods that should receive an idempotency key.
|
|
213
|
+
* Default: ['POST', 'PUT', 'PATCH', 'DELETE']
|
|
214
|
+
*/
|
|
215
|
+
idempotentMethods?: string[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Telemetry Meta Types ────────────────────────────
|
|
219
|
+
|
|
220
|
+
export interface RequestMeta {
|
|
221
|
+
method: string;
|
|
222
|
+
url: string;
|
|
223
|
+
timestamp: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface ResponseMeta {
|
|
227
|
+
method: string;
|
|
228
|
+
url: string;
|
|
229
|
+
status: number;
|
|
230
|
+
duration: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface ErrorMeta {
|
|
234
|
+
method: string;
|
|
235
|
+
url: string;
|
|
236
|
+
error: Record<string, unknown>;
|
|
237
|
+
duration: number;
|
|
238
|
+
attempt: number;
|
|
239
|
+
willRetry: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface ResolvedTransportConfig extends TransportConfig {
|
|
243
|
+
timeoutMs: number;
|
|
244
|
+
retries: RetryConfig;
|
|
245
|
+
plugins: SDKPlugin[];
|
|
246
|
+
idempotentMethods: string[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Per-request options that override transport-level defaults.
|
|
251
|
+
*/
|
|
252
|
+
export interface RequestOptions {
|
|
253
|
+
/** AbortSignal for cooperative cancellation of this specific request */
|
|
254
|
+
signal?: AbortSignal;
|
|
255
|
+
/** Per-request timeout override in milliseconds (overrides TransportConfig.timeoutMs) */
|
|
256
|
+
timeoutMs?: number;
|
|
257
|
+
/** Additional headers merged into this request only */
|
|
258
|
+
headers?: Record<string, string>;
|
|
259
|
+
/** Per-request retry override. Set `{ attempts: 0 }` to disable retries for this request. */
|
|
260
|
+
retries?: Partial<RetryConfig>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface SDKTransport {
|
|
264
|
+
readonly axios: AxiosInstance;
|
|
265
|
+
readonly config: ResolvedTransportConfig;
|
|
266
|
+
executeRequest<T>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Error Resolution ────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function resolveErrorCode(status: number): string {
|
|
272
|
+
switch (status) {
|
|
273
|
+
case 400: return 'bad_request';
|
|
274
|
+
case 401: return 'unauthorized';
|
|
275
|
+
case 403: return 'forbidden';
|
|
276
|
+
case 404: return 'not_found';
|
|
277
|
+
case 405: return 'method_not_allowed';
|
|
278
|
+
case 408: return 'request_timeout';
|
|
279
|
+
case 409: return 'conflict';
|
|
280
|
+
case 413: return 'payload_too_large';
|
|
281
|
+
case 422: return 'unprocessable_entity';
|
|
282
|
+
case 429: return 'rate_limit_exceeded';
|
|
283
|
+
case 502: return 'bad_gateway';
|
|
284
|
+
case 503: return 'service_unavailable';
|
|
285
|
+
case 504: return 'gateway_timeout';
|
|
286
|
+
default: return status >= 500 ? 'internal_server_error' : 'http_error';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveMessage(status: number): string {
|
|
291
|
+
switch (status) {
|
|
292
|
+
case 400: return 'Bad request';
|
|
293
|
+
case 401: return 'Unauthorized';
|
|
294
|
+
case 403: return 'Forbidden';
|
|
295
|
+
case 404: return 'Not found';
|
|
296
|
+
case 405: return 'Method not allowed';
|
|
297
|
+
case 408: return 'Request timeout';
|
|
298
|
+
case 409: return 'Conflict';
|
|
299
|
+
case 413: return 'Payload too large';
|
|
300
|
+
case 422: return 'Unprocessable entity';
|
|
301
|
+
case 429: return 'Rate limit exceeded';
|
|
302
|
+
case 502: return 'Bad gateway';
|
|
303
|
+
case 503: return 'Service unavailable';
|
|
304
|
+
case 504: return 'Gateway timeout';
|
|
305
|
+
default: return status >= 500 ? 'Internal server error' : 'HTTP error';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isAxiosError(error: unknown): error is AxiosError {
|
|
310
|
+
return !!error && typeof error === 'object' && (error as { isAxiosError?: unknown }).isAxiosError === true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readStartTime(config: AxiosRequestConfig | undefined): number | undefined {
|
|
314
|
+
const startTime = (config as { _startTime?: unknown } | undefined)?._startTime;
|
|
315
|
+
return typeof startTime === 'number' ? startTime : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function appendQueryParam(
|
|
319
|
+
reqConfig: InternalAxiosRequestConfig,
|
|
320
|
+
key: string,
|
|
321
|
+
value: string,
|
|
322
|
+
): void {
|
|
323
|
+
if (reqConfig.params instanceof URLSearchParams) {
|
|
324
|
+
reqConfig.params.set(key, value);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const params = reqConfig.params && typeof reqConfig.params === 'object'
|
|
329
|
+
? (reqConfig.params as Record<string, unknown>)
|
|
330
|
+
: {};
|
|
331
|
+
|
|
332
|
+
params[key] = value;
|
|
333
|
+
reqConfig.params = params;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function appendCookie(
|
|
337
|
+
reqConfig: InternalAxiosRequestConfig,
|
|
338
|
+
key: string,
|
|
339
|
+
value: string,
|
|
340
|
+
): void {
|
|
341
|
+
const serialized = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
342
|
+
const existing = reqConfig.headers.Cookie;
|
|
343
|
+
if (typeof existing === 'string' && existing.length > 0) {
|
|
344
|
+
reqConfig.headers.Cookie = `${existing}; ${serialized}`;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
reqConfig.headers.Cookie = serialized;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Transport Factory ───────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create a configured Axios-backed transport with automatic retries.
|
|
354
|
+
*/
|
|
355
|
+
export function createTransport(config: TransportConfig): SDKTransport {
|
|
356
|
+
const resolvedConfig: ResolvedTransportConfig = {
|
|
357
|
+
...config,
|
|
358
|
+
timeoutMs: config.timeoutMs ?? 15000,
|
|
359
|
+
retries: {
|
|
360
|
+
attempts: 3,
|
|
361
|
+
backoffMs: 400,
|
|
362
|
+
jitter: true,
|
|
363
|
+
maxBackoffMs: 30_000,
|
|
364
|
+
...config.retries,
|
|
365
|
+
},
|
|
366
|
+
plugins: config.plugins ?? [],
|
|
367
|
+
idempotentMethods: config.idempotentMethods ?? ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const instance = axios.create({
|
|
371
|
+
baseURL: resolvedConfig.baseUrl,
|
|
372
|
+
timeout: resolvedConfig.timeoutMs,
|
|
373
|
+
withCredentials: resolvedConfig.withCredentials ?? false,
|
|
374
|
+
headers: {
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
...(resolvedConfig.defaultHeaders ?? {}),
|
|
377
|
+
},
|
|
378
|
+
...resolvedConfig.axiosConfig,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
instance.interceptors.request.use(
|
|
382
|
+
async (reqConfig: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
|
383
|
+
(reqConfig as { _startTime?: number })._startTime = Date.now();
|
|
384
|
+
|
|
385
|
+
{{#each securitySchemes}}
|
|
386
|
+
{{#if (eq type "apiKey")}}
|
|
387
|
+
if (resolvedConfig.{{configKey}}) {
|
|
388
|
+
{{#if (eq in "header")}}
|
|
389
|
+
reqConfig.headers['{{paramName}}'] = resolvedConfig.{{configKey}};
|
|
390
|
+
{{/if}}
|
|
391
|
+
{{#if (eq in "query")}}
|
|
392
|
+
appendQueryParam(reqConfig, '{{paramName}}', resolvedConfig.{{configKey}});
|
|
393
|
+
{{/if}}
|
|
394
|
+
{{#if (eq in "cookie")}}
|
|
395
|
+
appendCookie(reqConfig, '{{paramName}}', resolvedConfig.{{configKey}});
|
|
396
|
+
{{/if}}
|
|
397
|
+
}
|
|
398
|
+
{{/if}}
|
|
399
|
+
{{#if (and (eq type "http") (eq scheme "bearer"))}}
|
|
400
|
+
if (resolvedConfig.{{configKey}}) {
|
|
401
|
+
reqConfig.headers.Authorization = `Bearer ${ resolvedConfig.{{configKey}} }`;
|
|
402
|
+
}
|
|
403
|
+
{{/if}}
|
|
404
|
+
{{#if (eq type "oauth2")}}
|
|
405
|
+
if (resolvedConfig.{{configKey}}) {
|
|
406
|
+
reqConfig.headers.Authorization = `Bearer ${ resolvedConfig.{{configKey}} }`;
|
|
407
|
+
}
|
|
408
|
+
{{/if}}
|
|
409
|
+
{{#if (eq type "openIdConnect")}}
|
|
410
|
+
if (resolvedConfig.{{configKey}}) {
|
|
411
|
+
reqConfig.headers.Authorization = `Bearer ${ resolvedConfig.{{configKey}} }`;
|
|
412
|
+
}
|
|
413
|
+
{{/if}}
|
|
414
|
+
{{/each}}
|
|
415
|
+
|
|
416
|
+
// ── Static Cookies ─────────────────────────────────
|
|
417
|
+
if (resolvedConfig.cookies) {
|
|
418
|
+
for (const [name, value] of Object.entries(resolvedConfig.cookies)) {
|
|
419
|
+
appendCookie(reqConfig, name, value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const method = reqConfig.method?.toUpperCase();
|
|
424
|
+
if (
|
|
425
|
+
method &&
|
|
426
|
+
resolvedConfig.idempotentMethods.includes(method) &&
|
|
427
|
+
resolvedConfig.getIdempotencyKey &&
|
|
428
|
+
!reqConfig.headers['X-Idempotency-Key']
|
|
429
|
+
) {
|
|
430
|
+
reqConfig.headers['X-Idempotency-Key'] = resolvedConfig.getIdempotencyKey();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
resolvedConfig.onRequest?.({
|
|
434
|
+
method: reqConfig.method || 'GET',
|
|
435
|
+
url: reqConfig.url || '',
|
|
436
|
+
timestamp: (reqConfig as { _startTime?: number })._startTime ?? Date.now(),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ── Plugin: beforeRequest ───────────────────────────
|
|
440
|
+
let result: InternalAxiosRequestConfig = reqConfig;
|
|
441
|
+
for (const plugin of resolvedConfig.plugins) {
|
|
442
|
+
if (plugin.beforeRequest) {
|
|
443
|
+
const returned = await plugin.beforeRequest(result);
|
|
444
|
+
if (returned !== undefined) result = returned;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
},
|
|
449
|
+
(error) => Promise.reject(error),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
instance.interceptors.response.use(async (response) => {
|
|
453
|
+
const startTime = readStartTime(response.config);
|
|
454
|
+
const duration = startTime === undefined ? 0 : Date.now() - startTime;
|
|
455
|
+
|
|
456
|
+
resolvedConfig.onResponse?.({
|
|
457
|
+
method: response.config.method || 'GET',
|
|
458
|
+
url: response.config.url || '',
|
|
459
|
+
status: response.status,
|
|
460
|
+
duration,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ── Plugin: afterResponse ───────────────────────────
|
|
464
|
+
let result: AxiosResponse = response;
|
|
465
|
+
for (const plugin of resolvedConfig.plugins) {
|
|
466
|
+
if (plugin.afterResponse) {
|
|
467
|
+
const returned = await plugin.afterResponse(result);
|
|
468
|
+
if (returned !== undefined) result = returned;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const transport: SDKTransport = {
|
|
475
|
+
axios: instance,
|
|
476
|
+
config: resolvedConfig,
|
|
477
|
+
executeRequest: <T>(requestConfig: AxiosRequestConfig, options?: RequestOptions) =>
|
|
478
|
+
executeRequest<T>(transport, requestConfig, options),
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
return transport;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Request Execution ───────────────────────────────
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Execute an HTTP request with automatic retries and error mapping.
|
|
488
|
+
*/
|
|
489
|
+
export async function executeRequest<T>(
|
|
490
|
+
transport: SDKTransport,
|
|
491
|
+
requestConfig: AxiosRequestConfig,
|
|
492
|
+
options?: RequestOptions,
|
|
493
|
+
): Promise<T> {
|
|
494
|
+
const retries: RetryConfig = {
|
|
495
|
+
...transport.config.retries,
|
|
496
|
+
...options?.retries,
|
|
497
|
+
};
|
|
498
|
+
const maxAttempts = retries.attempts;
|
|
499
|
+
const maxBackoff = retries.maxBackoffMs ?? 30_000;
|
|
500
|
+
const mapError = transport.config.mapError ?? mapAxiosError;
|
|
501
|
+
|
|
502
|
+
const mergedConfig: AxiosRequestConfig = {
|
|
503
|
+
...requestConfig,
|
|
504
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
505
|
+
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
506
|
+
...(options?.headers
|
|
507
|
+
? { headers: { ...requestConfig.headers, ...options.headers } }
|
|
508
|
+
: {}),
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
let lastError: SDKError | undefined;
|
|
512
|
+
|
|
513
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
514
|
+
// Check for cancellation before each attempt
|
|
515
|
+
if (options?.signal?.aborted) {
|
|
516
|
+
throw lastError ?? new SDKError('Request aborted', 0, 'aborted');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const response = await transport.axios.request<T>(mergedConfig);
|
|
521
|
+
|
|
522
|
+
// Handle no-content responses
|
|
523
|
+
if (response.status === 204 || response.status === 205) {
|
|
524
|
+
return undefined as T;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Apply custom response transform if provided
|
|
528
|
+
if (transport.config.responseTransform) {
|
|
529
|
+
return transport.config.responseTransform<T>(response);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return response.data;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
// Don't retry if the request was cancelled
|
|
535
|
+
if (options?.signal?.aborted) {
|
|
536
|
+
throw mapError(error);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const sdkError = mapError(error);
|
|
540
|
+
lastError = sdkError;
|
|
541
|
+
|
|
542
|
+
const axiosConfig = isAxiosError(error) ? error.config : undefined;
|
|
543
|
+
const effectiveConfig = axiosConfig ?? mergedConfig;
|
|
544
|
+
const startTime = readStartTime(effectiveConfig);
|
|
545
|
+
const duration = startTime === undefined ? 0 : Date.now() - startTime;
|
|
546
|
+
|
|
547
|
+
// Determine if we should retry
|
|
548
|
+
const shouldRetry = retries.shouldRetry
|
|
549
|
+
? retries.shouldRetry(sdkError, attempt)
|
|
550
|
+
: sdkError.isRetryable();
|
|
551
|
+
|
|
552
|
+
const willRetry = attempt < maxAttempts && shouldRetry;
|
|
553
|
+
|
|
554
|
+
// Notify error hook
|
|
555
|
+
transport.config.onError?.({
|
|
556
|
+
method: effectiveConfig.method || 'GET',
|
|
557
|
+
url: effectiveConfig.url || '',
|
|
558
|
+
error: sdkError.toJSON(),
|
|
559
|
+
duration,
|
|
560
|
+
attempt,
|
|
561
|
+
willRetry,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (!willRetry) {
|
|
565
|
+
// Run plugin onError hooks before throwing
|
|
566
|
+
let finalError = sdkError;
|
|
567
|
+
for (const plugin of transport.config.plugins) {
|
|
568
|
+
if (plugin.onError) {
|
|
569
|
+
const transformed = await plugin.onError(finalError, mergedConfig);
|
|
570
|
+
if (transformed instanceof SDKError) finalError = transformed;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
throw finalError;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Calculate delay
|
|
577
|
+
const retryAfterMs = parseRetryAfterMs(error);
|
|
578
|
+
let delayMs: number;
|
|
579
|
+
|
|
580
|
+
if (sdkError.status === 429 && retryAfterMs !== undefined) {
|
|
581
|
+
delayMs = retryAfterMs;
|
|
582
|
+
} else {
|
|
583
|
+
delayMs = calculateBackoff(attempt, retries.backoffMs, retries.jitter, maxBackoff);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Allow custom delay override
|
|
587
|
+
if (retries.delay) {
|
|
588
|
+
delayMs = retries.delay(sdkError, attempt, delayMs);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Notify retry hook (can abort)
|
|
592
|
+
if (retries.onRetry) {
|
|
593
|
+
const result = await retries.onRetry(sdkError, attempt, delayMs);
|
|
594
|
+
if (result === false) {
|
|
595
|
+
throw sdkError;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
await delay(delayMs, options?.signal);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Should never reach here, but TypeScript needs it
|
|
604
|
+
throw lastError ?? new SDKError('Request failed after retries', 0, 'retry_exhausted');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Map an Axios error to a structured SDKError.
|
|
609
|
+
*/
|
|
610
|
+
function mapAxiosError(error: unknown): SDKError {
|
|
611
|
+
if (error instanceof SDKError) {
|
|
612
|
+
return error;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!isAxiosError(error)) {
|
|
616
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
617
|
+
return new SDKError('Request aborted', 0, 'aborted');
|
|
618
|
+
}
|
|
619
|
+
return new SDKError(
|
|
620
|
+
error instanceof Error ? error.message : 'Unexpected error',
|
|
621
|
+
0,
|
|
622
|
+
'unexpected_error',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (error.code === 'ERR_CANCELED' || error.code === 'ECONNABORTED') {
|
|
627
|
+
if (error.message?.includes('timeout')) {
|
|
628
|
+
return new SDKError('Request timed out', 504, 'gateway_timeout');
|
|
629
|
+
}
|
|
630
|
+
return new SDKError('Request aborted', 0, 'aborted');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (error.code === 'ECONNREFUSED') {
|
|
634
|
+
return new SDKError('Connection refused', 0, 'connection_refused');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (error.code === 'ENOTFOUND') {
|
|
638
|
+
return new SDKError('DNS lookup failed', 0, 'dns_error');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!error.response) {
|
|
642
|
+
return new SDKError(
|
|
643
|
+
error.message || 'Unable to connect to the server',
|
|
644
|
+
0,
|
|
645
|
+
'network_error',
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const status = error.response.status;
|
|
650
|
+
const data = error.response.data as any;
|
|
651
|
+
|
|
652
|
+
if (data && typeof data === 'object' && typeof data.error === 'string' && typeof data.message === 'string') {
|
|
653
|
+
return new SDKError(data as ApiErrorEnvelope, status);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return new SDKError(
|
|
657
|
+
data?.message || error.message || resolveMessage(status),
|
|
658
|
+
status,
|
|
659
|
+
data?.error || resolveErrorCode(status),
|
|
660
|
+
data?.request_id,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["index.ts", "src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|