@rapidd/core 2.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/.dockerignore +71 -0
- package/.env.example +70 -0
- package/.gitignore +11 -0
- package/LICENSE +15 -0
- package/README.md +231 -0
- package/bin/cli.js +145 -0
- package/config/app.json +166 -0
- package/config/rate-limit.json +12 -0
- package/dist/main.js +26 -0
- package/dockerfile +57 -0
- package/locales/ar_SA.json +179 -0
- package/locales/de_DE.json +179 -0
- package/locales/en_US.json +180 -0
- package/locales/es_ES.json +179 -0
- package/locales/fr_FR.json +179 -0
- package/locales/it_IT.json +179 -0
- package/locales/ja_JP.json +179 -0
- package/locales/pt_BR.json +179 -0
- package/locales/ru_RU.json +179 -0
- package/locales/tr_TR.json +179 -0
- package/main.ts +25 -0
- package/package.json +126 -0
- package/prisma/schema.prisma +9 -0
- package/prisma.config.ts +12 -0
- package/public/static/favicon.ico +0 -0
- package/public/static/image/logo.png +0 -0
- package/routes/api/v1/index.ts +113 -0
- package/src/app.ts +197 -0
- package/src/auth/Auth.ts +446 -0
- package/src/auth/stores/ISessionStore.ts +19 -0
- package/src/auth/stores/MemoryStore.ts +70 -0
- package/src/auth/stores/RedisStore.ts +92 -0
- package/src/auth/stores/index.ts +149 -0
- package/src/config/acl.ts +9 -0
- package/src/config/rls.ts +38 -0
- package/src/core/dmmf.ts +226 -0
- package/src/core/env.ts +183 -0
- package/src/core/errors.ts +87 -0
- package/src/core/i18n.ts +144 -0
- package/src/core/middleware.ts +123 -0
- package/src/core/prisma.ts +236 -0
- package/src/index.ts +112 -0
- package/src/middleware/model.ts +61 -0
- package/src/orm/Model.ts +881 -0
- package/src/orm/QueryBuilder.ts +2078 -0
- package/src/plugins/auth.ts +162 -0
- package/src/plugins/language.ts +79 -0
- package/src/plugins/rateLimit.ts +210 -0
- package/src/plugins/response.ts +80 -0
- package/src/plugins/rls.ts +51 -0
- package/src/plugins/security.ts +23 -0
- package/src/plugins/upload.ts +299 -0
- package/src/types.ts +308 -0
- package/src/utils/ApiClient.ts +526 -0
- package/src/utils/Mailer.ts +348 -0
- package/src/utils/index.ts +25 -0
- package/templates/email/example.ejs +17 -0
- package/templates/layouts/email.ejs +35 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ServiceConfig {
|
|
6
|
+
hostname: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
secure?: boolean;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
queries?: Record<string, string>;
|
|
12
|
+
authorization?: AuthConfig;
|
|
13
|
+
endpoints?: Record<string, EndpointConfig>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EndpointConfig {
|
|
17
|
+
path: string;
|
|
18
|
+
method?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
queries?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthConfig {
|
|
24
|
+
type: 'basic' | 'bearer' | 'api-key' | 'oauth2';
|
|
25
|
+
'auth-header'?: string;
|
|
26
|
+
// Basic
|
|
27
|
+
username?: string;
|
|
28
|
+
password?: string;
|
|
29
|
+
// Bearer / X-Auth
|
|
30
|
+
token?: string;
|
|
31
|
+
// API Key
|
|
32
|
+
key?: string;
|
|
33
|
+
'key-header'?: string;
|
|
34
|
+
// OAuth2
|
|
35
|
+
hostname?: string;
|
|
36
|
+
token_path?: string;
|
|
37
|
+
client_id?: string;
|
|
38
|
+
client_secret?: string;
|
|
39
|
+
grant_type?: string;
|
|
40
|
+
scope?: string;
|
|
41
|
+
secure?: boolean;
|
|
42
|
+
params?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RequestOptions {
|
|
46
|
+
params?: Record<string, string>;
|
|
47
|
+
queries?: Record<string, unknown>;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
body?: unknown;
|
|
50
|
+
timeout?: number;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ApiResponse<T = unknown> {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
status: number;
|
|
57
|
+
data: T;
|
|
58
|
+
headers: Headers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ApiClientError extends Error {
|
|
62
|
+
status: number;
|
|
63
|
+
response: unknown;
|
|
64
|
+
url: string;
|
|
65
|
+
|
|
66
|
+
constructor(message: string, status: number, response: unknown, url: string) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = 'ApiClientError';
|
|
69
|
+
this.status = status;
|
|
70
|
+
this.response = response;
|
|
71
|
+
this.url = url;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const REQUEST_TIMEOUT = parseInt(process.env.REQUEST_TIMEOUT || '10000', 10);
|
|
78
|
+
const MAX_RETRIES = parseInt(process.env.API_MAX_RETRIES || '2', 10);
|
|
79
|
+
|
|
80
|
+
let _config: { services?: Record<string, ServiceConfig> } = { services: {} };
|
|
81
|
+
let _configLoaded = false;
|
|
82
|
+
const _tokenCache = new Map<string, { access_token: string; token_type: string; expires: number }>();
|
|
83
|
+
|
|
84
|
+
function getConfig(): { services?: Record<string, ServiceConfig> } {
|
|
85
|
+
if (!_configLoaded) {
|
|
86
|
+
try {
|
|
87
|
+
_config = require(path.join(process.cwd(), 'config', 'app.json'));
|
|
88
|
+
} catch {
|
|
89
|
+
_config = { services: {} };
|
|
90
|
+
}
|
|
91
|
+
_configLoaded = true;
|
|
92
|
+
}
|
|
93
|
+
return _config;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getService(name: string): ServiceConfig {
|
|
97
|
+
const config = getConfig();
|
|
98
|
+
const service = config.services?.[name];
|
|
99
|
+
if (!service) {
|
|
100
|
+
const available = Object.keys(config.services || {}).join(', ') || 'none';
|
|
101
|
+
throw new Error(`Service '${name}' not found. Available: ${available}`);
|
|
102
|
+
}
|
|
103
|
+
return service;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function resolvePath(template: string, params: Record<string, string> = {}): string {
|
|
109
|
+
return template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
|
|
110
|
+
const trimmed = key.trim();
|
|
111
|
+
if (!(trimmed in params)) {
|
|
112
|
+
throw new Error(`Missing path parameter: ${trimmed}`);
|
|
113
|
+
}
|
|
114
|
+
return encodeURIComponent(params[trimmed]);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildUrl(
|
|
119
|
+
service: ServiceConfig,
|
|
120
|
+
endpointPath: string,
|
|
121
|
+
queries: Record<string, unknown> = {}
|
|
122
|
+
): string {
|
|
123
|
+
const protocol = service.secure !== false ? 'https' : 'http';
|
|
124
|
+
const port = service.port ? `:${service.port}` : '';
|
|
125
|
+
const basePath = service.path || '';
|
|
126
|
+
|
|
127
|
+
const url = new URL(`${protocol}://${service.hostname}${port}${basePath}${endpointPath}`);
|
|
128
|
+
|
|
129
|
+
const allQueries = { ...(service.queries || {}), ...queries };
|
|
130
|
+
for (const [key, value] of Object.entries(allQueries)) {
|
|
131
|
+
if (value !== undefined && value !== null) {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
value.forEach((v, i) => url.searchParams.append(`${key}[${i}]`, String(v)));
|
|
134
|
+
} else if (typeof value === 'object') {
|
|
135
|
+
for (const [k, v] of Object.entries(value)) {
|
|
136
|
+
url.searchParams.append(`${key}[${k}]`, String(v));
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
url.searchParams.append(key, String(value));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return url.toString();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getOAuthToken(serviceName: string, auth: AuthConfig): Promise<{ access_token: string; token_type: string }> {
|
|
148
|
+
const cached = _tokenCache.get(serviceName);
|
|
149
|
+
if (cached && cached.expires > Date.now() + 60000) {
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tokenUrl = `${auth.secure !== false ? 'https' : 'http'}://${auth.hostname}${auth.token_path}`;
|
|
154
|
+
|
|
155
|
+
const body = new URLSearchParams({
|
|
156
|
+
grant_type: auth.grant_type || 'client_credentials',
|
|
157
|
+
client_id: auth.client_id!,
|
|
158
|
+
client_secret: auth.client_secret!,
|
|
159
|
+
...(auth.scope && { scope: auth.scope }),
|
|
160
|
+
...(auth.params || {})
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const response = await fetch(tokenUrl, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
167
|
+
'Accept': 'application/json'
|
|
168
|
+
},
|
|
169
|
+
body
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const text = await response.text();
|
|
174
|
+
throw new Error(`OAuth2 token request failed (${response.status}): ${text}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const tokenData = await response.json() as { access_token: string; token_type?: string; expires_in?: number };
|
|
178
|
+
|
|
179
|
+
if (!tokenData.access_token) {
|
|
180
|
+
throw new Error('Invalid OAuth2 response: missing access_token');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const expiresIn = tokenData.expires_in || 3600;
|
|
184
|
+
const token = {
|
|
185
|
+
access_token: tokenData.access_token,
|
|
186
|
+
token_type: tokenData.token_type || 'Bearer',
|
|
187
|
+
expires: Date.now() + (expiresIn * 900) // 90% of actual expiry
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
_tokenCache.set(serviceName, token);
|
|
191
|
+
return token;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function applyAuth(
|
|
195
|
+
headers: Record<string, string>,
|
|
196
|
+
serviceName: string,
|
|
197
|
+
auth: AuthConfig
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
const authHeader = auth['auth-header'] || 'Authorization';
|
|
200
|
+
|
|
201
|
+
switch (auth.type) {
|
|
202
|
+
case 'basic': {
|
|
203
|
+
if (!auth.username || !auth.password) {
|
|
204
|
+
throw new Error('Basic auth requires username and password');
|
|
205
|
+
}
|
|
206
|
+
const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
|
|
207
|
+
headers[authHeader] = `Basic ${credentials}`;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case 'bearer': {
|
|
211
|
+
if (!auth.token) throw new Error('Bearer auth requires token');
|
|
212
|
+
headers[authHeader] = `Bearer ${auth.token}`;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 'api-key': {
|
|
216
|
+
if (!auth.key) throw new Error('API key auth requires key');
|
|
217
|
+
headers[auth['key-header'] || 'X-API-Key'] = auth.key;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'oauth2': {
|
|
221
|
+
const token = await getOAuthToken(serviceName, auth);
|
|
222
|
+
headers[authHeader] = `${token.token_type} ${token.access_token}`;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function parseResponse(response: Response): Promise<unknown> {
|
|
229
|
+
const contentType = response.headers.get('content-type') || '';
|
|
230
|
+
|
|
231
|
+
if (contentType.includes('application/json')) {
|
|
232
|
+
return response.json();
|
|
233
|
+
}
|
|
234
|
+
if (contentType.includes('text/')) {
|
|
235
|
+
return response.text();
|
|
236
|
+
}
|
|
237
|
+
return response.arrayBuffer();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Main API ─────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Lightweight, config-driven API client.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* // Using predefined service/endpoint from config/app.json
|
|
247
|
+
* const user = await ApiClient.call('GoogleAPI', 'getUserInfo', {
|
|
248
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
249
|
+
* });
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // Ad-hoc request
|
|
253
|
+
* const data = await ApiClient.fetch('https://api.example.com/users', {
|
|
254
|
+
* method: 'POST',
|
|
255
|
+
* body: { name: 'John' }
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* // Fluent builder
|
|
260
|
+
* const response = await ApiClient.to('https://api.example.com')
|
|
261
|
+
* .bearer(token)
|
|
262
|
+
* .post('/users', { name: 'John' });
|
|
263
|
+
*/
|
|
264
|
+
export const ApiClient = {
|
|
265
|
+
/**
|
|
266
|
+
* Call a predefined service endpoint from config/app.json
|
|
267
|
+
*/
|
|
268
|
+
async call<T = unknown>(
|
|
269
|
+
serviceName: string,
|
|
270
|
+
endpointName: string,
|
|
271
|
+
options: RequestOptions = {}
|
|
272
|
+
): Promise<T> {
|
|
273
|
+
const service = getService(serviceName);
|
|
274
|
+
const endpoint = service.endpoints?.[endpointName];
|
|
275
|
+
|
|
276
|
+
if (!endpoint) {
|
|
277
|
+
const available = Object.keys(service.endpoints || {}).join(', ') || 'none';
|
|
278
|
+
throw new Error(`Endpoint '${endpointName}' not found in '${serviceName}'. Available: ${available}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const resolvedPath = resolvePath(endpoint.path, options.params);
|
|
282
|
+
const url = buildUrl(service, resolvedPath, {
|
|
283
|
+
...(endpoint.queries || {}),
|
|
284
|
+
...(options.queries || {})
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const headers: Record<string, string> = {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
'Accept': 'application/json',
|
|
290
|
+
...(service.headers || {}),
|
|
291
|
+
...(endpoint.headers || {}),
|
|
292
|
+
...(options.headers || {})
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (service.authorization) {
|
|
296
|
+
await applyAuth(headers, serviceName, service.authorization);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this.fetch<T>(url, {
|
|
300
|
+
method: endpoint.method || 'GET',
|
|
301
|
+
headers,
|
|
302
|
+
body: options.body,
|
|
303
|
+
timeout: options.timeout,
|
|
304
|
+
signal: options.signal
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Make an ad-hoc fetch request with retries and error handling
|
|
310
|
+
*/
|
|
311
|
+
async fetch<T = unknown>(
|
|
312
|
+
url: string,
|
|
313
|
+
options: {
|
|
314
|
+
method?: string;
|
|
315
|
+
headers?: Record<string, string>;
|
|
316
|
+
body?: unknown;
|
|
317
|
+
timeout?: number;
|
|
318
|
+
signal?: AbortSignal;
|
|
319
|
+
retries?: number;
|
|
320
|
+
} = {}
|
|
321
|
+
): Promise<T> {
|
|
322
|
+
const { method = 'GET', headers = {}, body, timeout = REQUEST_TIMEOUT, retries = MAX_RETRIES } = options;
|
|
323
|
+
|
|
324
|
+
const fetchOptions: RequestInit = {
|
|
325
|
+
method,
|
|
326
|
+
headers: {
|
|
327
|
+
'Content-Type': 'application/json',
|
|
328
|
+
'Accept': 'application/json',
|
|
329
|
+
...headers
|
|
330
|
+
},
|
|
331
|
+
signal: options.signal
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (body !== undefined && !['GET', 'HEAD'].includes(method)) {
|
|
335
|
+
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let lastError: Error | null = null;
|
|
339
|
+
|
|
340
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
341
|
+
const controller = new AbortController();
|
|
342
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
343
|
+
|
|
344
|
+
if (!options.signal) {
|
|
345
|
+
fetchOptions.signal = controller.signal;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetch(url, fetchOptions);
|
|
350
|
+
clearTimeout(timeoutId);
|
|
351
|
+
|
|
352
|
+
const data = await parseResponse(response);
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new ApiClientError(
|
|
356
|
+
`HTTP ${response.status}`,
|
|
357
|
+
response.status,
|
|
358
|
+
data,
|
|
359
|
+
url
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return data as T;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
clearTimeout(timeoutId);
|
|
366
|
+
lastError = error as Error;
|
|
367
|
+
|
|
368
|
+
// Don't retry client errors (4xx)
|
|
369
|
+
if (error instanceof ApiClientError && error.status >= 400 && error.status < 500) {
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Retry with exponential backoff
|
|
374
|
+
if (attempt < retries) {
|
|
375
|
+
await new Promise(r => setTimeout(r, Math.min(1000 * Math.pow(2, attempt), 5000)));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
throw lastError;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Create a fluent request builder for ad-hoc requests
|
|
385
|
+
*/
|
|
386
|
+
to(baseUrl: string) {
|
|
387
|
+
return new RequestBuilder(baseUrl);
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Clear OAuth token cache (useful for testing or token revocation)
|
|
392
|
+
*/
|
|
393
|
+
clearTokenCache(serviceName?: string): void {
|
|
394
|
+
if (serviceName) {
|
|
395
|
+
_tokenCache.delete(serviceName);
|
|
396
|
+
} else {
|
|
397
|
+
_tokenCache.clear();
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* List available services from config
|
|
403
|
+
*/
|
|
404
|
+
listServices(): string[] {
|
|
405
|
+
return Object.keys(getConfig().services || {});
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* List endpoints for a service
|
|
410
|
+
*/
|
|
411
|
+
listEndpoints(serviceName: string): string[] {
|
|
412
|
+
const service = getService(serviceName);
|
|
413
|
+
return Object.keys(service.endpoints || {});
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// ── Fluent Builder ───────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
class RequestBuilder {
|
|
420
|
+
private _baseUrl: string;
|
|
421
|
+
private _headers: Record<string, string> = {};
|
|
422
|
+
private _timeout: number = REQUEST_TIMEOUT;
|
|
423
|
+
private _retries: number = MAX_RETRIES;
|
|
424
|
+
|
|
425
|
+
constructor(baseUrl: string) {
|
|
426
|
+
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
header(key: string, value: string): this {
|
|
430
|
+
this._headers[key] = value;
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
headers(headers: Record<string, string>): this {
|
|
435
|
+
Object.assign(this._headers, headers);
|
|
436
|
+
return this;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
bearer(token: string): this {
|
|
440
|
+
this._headers['Authorization'] = `Bearer ${token}`;
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
basic(username: string, password: string): this {
|
|
445
|
+
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
446
|
+
this._headers['Authorization'] = `Basic ${credentials}`;
|
|
447
|
+
return this;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
apiKey(key: string, header = 'X-API-Key'): this {
|
|
451
|
+
this._headers[header] = key;
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
timeout(ms: number): this {
|
|
456
|
+
this._timeout = ms;
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
retries(count: number): this {
|
|
461
|
+
this._retries = count;
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private _url(path: string, queries?: Record<string, unknown>): string {
|
|
466
|
+
const url = new URL(path.startsWith('/') ? `${this._baseUrl}${path}` : path);
|
|
467
|
+
if (queries) {
|
|
468
|
+
for (const [key, value] of Object.entries(queries)) {
|
|
469
|
+
if (value !== undefined && value !== null) {
|
|
470
|
+
url.searchParams.append(key, String(value));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return url.toString();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async get<T = unknown>(path: string, queries?: Record<string, unknown>): Promise<T> {
|
|
478
|
+
return ApiClient.fetch<T>(this._url(path, queries), {
|
|
479
|
+
method: 'GET',
|
|
480
|
+
headers: this._headers,
|
|
481
|
+
timeout: this._timeout,
|
|
482
|
+
retries: this._retries
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async post<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
487
|
+
return ApiClient.fetch<T>(this._url(path), {
|
|
488
|
+
method: 'POST',
|
|
489
|
+
headers: this._headers,
|
|
490
|
+
body,
|
|
491
|
+
timeout: this._timeout,
|
|
492
|
+
retries: this._retries
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async put<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
497
|
+
return ApiClient.fetch<T>(this._url(path), {
|
|
498
|
+
method: 'PUT',
|
|
499
|
+
headers: this._headers,
|
|
500
|
+
body,
|
|
501
|
+
timeout: this._timeout,
|
|
502
|
+
retries: this._retries
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async patch<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
507
|
+
return ApiClient.fetch<T>(this._url(path), {
|
|
508
|
+
method: 'PATCH',
|
|
509
|
+
headers: this._headers,
|
|
510
|
+
body,
|
|
511
|
+
timeout: this._timeout,
|
|
512
|
+
retries: this._retries
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async delete<T = unknown>(path: string): Promise<T> {
|
|
517
|
+
return ApiClient.fetch<T>(this._url(path), {
|
|
518
|
+
method: 'DELETE',
|
|
519
|
+
headers: this._headers,
|
|
520
|
+
timeout: this._timeout,
|
|
521
|
+
retries: this._retries
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export default ApiClient;
|