kimu-core 0.4.1

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.
Files changed (146) hide show
  1. package/.editorconfig +30 -0
  2. package/.gitattributes +11 -0
  3. package/.github/FUNDING.yml +8 -0
  4. package/.github/copilot-instructions.md +103 -0
  5. package/.github/kimu-copilot-instructions.md +3779 -0
  6. package/.github/workflows/deploy-demo.yml +39 -0
  7. package/AUTHORS.md +20 -0
  8. package/CHANGELOG.md +20 -0
  9. package/CODE_GUIDELINES.md +165 -0
  10. package/CODE_OF_CONDUCT.md +47 -0
  11. package/CONTRIBUTING.md +62 -0
  12. package/FUNDING.md +31 -0
  13. package/ISSUE_GUIDELINES.md +74 -0
  14. package/LICENSE +17 -0
  15. package/LICENSE.it.md +17 -0
  16. package/MPL-2.0.txt +373 -0
  17. package/NOTICE +65 -0
  18. package/README-KIMU.md +40 -0
  19. package/README.it.md +208 -0
  20. package/README.md +266 -0
  21. package/SECURITY.md +64 -0
  22. package/docs/get-started-en.md +207 -0
  23. package/docs/images/icon.svg +64 -0
  24. package/docs/images/logo_kimu.png +0 -0
  25. package/docs/index.md +29 -0
  26. package/env/dev.config.json +6 -0
  27. package/env/local.config.json +6 -0
  28. package/env/prod.config.json +6 -0
  29. package/env/staging.config.json +6 -0
  30. package/env/test.config.json +4 -0
  31. package/icon.svg +10 -0
  32. package/logo_kimu.png +0 -0
  33. package/package.json +79 -0
  34. package/public/favicon.svg +64 -0
  35. package/public/logo_kimu.svg +1 -0
  36. package/scripts/build-all-config.js +59 -0
  37. package/scripts/build-all-core.js +65 -0
  38. package/scripts/build-all-extensions.js +64 -0
  39. package/scripts/build-all-modules.js +99 -0
  40. package/scripts/build-extension.js +60 -0
  41. package/scripts/clear-kimu-build.js +31 -0
  42. package/scripts/generate-kimu-build-config.js +79 -0
  43. package/scripts/install-module.js +162 -0
  44. package/scripts/list-modules.js +109 -0
  45. package/scripts/minify-css-assets.js +82 -0
  46. package/scripts/remove-module.js +122 -0
  47. package/scripts/utils/fix-imports.js +85 -0
  48. package/src/assets/index.css +43 -0
  49. package/src/assets/kimu-style.css +84 -0
  50. package/src/assets/style.css +116 -0
  51. package/src/config/kimu-base-config.json +5 -0
  52. package/src/core/index.ts +47 -0
  53. package/src/core/kimu-app.ts +76 -0
  54. package/src/core/kimu-asset-manager.ts +167 -0
  55. package/src/core/kimu-component-element.ts +325 -0
  56. package/src/core/kimu-component.ts +33 -0
  57. package/src/core/kimu-engine.ts +188 -0
  58. package/src/core/kimu-extension-manager.ts +281 -0
  59. package/src/core/kimu-global-styles.ts +136 -0
  60. package/src/core/kimu-module-manager.ts +69 -0
  61. package/src/core/kimu-module.ts +21 -0
  62. package/src/core/kimu-path-config.ts +127 -0
  63. package/src/core/kimu-reactive.ts +196 -0
  64. package/src/core/kimu-render.ts +91 -0
  65. package/src/core/kimu-store.ts +147 -0
  66. package/src/core/kimu-types.ts +65 -0
  67. package/src/extensions/.gitkeep +0 -0
  68. package/src/extensions/extensions-manifest.json +13 -0
  69. package/src/extensions/kimu-home/component.ts +80 -0
  70. package/src/extensions/kimu-home/lang/en.json +5 -0
  71. package/src/extensions/kimu-home/lang/it.json +5 -0
  72. package/src/extensions/kimu-home/style.css +61 -0
  73. package/src/extensions/kimu-home/view.html +51 -0
  74. package/src/index.html +26 -0
  75. package/src/main.ts +68 -0
  76. package/src/modules/.gitkeep +0 -0
  77. package/src/modules/README.md +79 -0
  78. package/src/modules/i18n/README.it.md +63 -0
  79. package/src/modules/i18n/README.md +63 -0
  80. package/src/modules/i18n/kimu-global-lang.ts +26 -0
  81. package/src/modules/i18n/kimu-i18n-service.ts +108 -0
  82. package/src/modules/i18n/manifest.json +22 -0
  83. package/src/modules/i18n/module.ts +39 -0
  84. package/src/modules/modules-manifest.json +12 -0
  85. package/src/modules-repository/README.md +108 -0
  86. package/src/modules-repository/api-axios/CHANGELOG.md +48 -0
  87. package/src/modules-repository/api-axios/QUICK-REFERENCE.md +178 -0
  88. package/src/modules-repository/api-axios/README.md +304 -0
  89. package/src/modules-repository/api-axios/api-axios-service.ts +355 -0
  90. package/src/modules-repository/api-axios/examples.ts +293 -0
  91. package/src/modules-repository/api-axios/index.ts +19 -0
  92. package/src/modules-repository/api-axios/interfaces.ts +71 -0
  93. package/src/modules-repository/api-axios/module.ts +41 -0
  94. package/src/modules-repository/api-core/CHANGELOG.md +42 -0
  95. package/src/modules-repository/api-core/QUICK-REFERENCE.md +192 -0
  96. package/src/modules-repository/api-core/README.md +435 -0
  97. package/src/modules-repository/api-core/api-core-service.ts +289 -0
  98. package/src/modules-repository/api-core/examples.ts +432 -0
  99. package/src/modules-repository/api-core/index.ts +8 -0
  100. package/src/modules-repository/api-core/interfaces.ts +83 -0
  101. package/src/modules-repository/api-core/module.ts +30 -0
  102. package/src/modules-repository/event-bus/README.md +273 -0
  103. package/src/modules-repository/event-bus/event-bus-service.ts +176 -0
  104. package/src/modules-repository/event-bus/module.ts +30 -0
  105. package/src/modules-repository/i18n/README.it.md +63 -0
  106. package/src/modules-repository/i18n/README.md +63 -0
  107. package/src/modules-repository/i18n/kimu-global-lang.ts +26 -0
  108. package/src/modules-repository/i18n/kimu-i18n-service.ts +108 -0
  109. package/src/modules-repository/i18n/manifest.json +22 -0
  110. package/src/modules-repository/i18n/module.ts +39 -0
  111. package/src/modules-repository/notification/README.md +423 -0
  112. package/src/modules-repository/notification/module.ts +30 -0
  113. package/src/modules-repository/notification/notification-service.ts +436 -0
  114. package/src/modules-repository/router/README.it.md +39 -0
  115. package/src/modules-repository/router/README.md +39 -0
  116. package/src/modules-repository/router/manifest.json +21 -0
  117. package/src/modules-repository/router/module.ts +23 -0
  118. package/src/modules-repository/router/router.ts +144 -0
  119. package/src/modules-repository/state/README.md +409 -0
  120. package/src/modules-repository/state/module.ts +30 -0
  121. package/src/modules-repository/state/state-service.ts +296 -0
  122. package/src/modules-repository/theme/README.md +267 -0
  123. package/src/modules-repository/theme/module.ts +30 -0
  124. package/src/modules-repository/theme/pre-build.js +40 -0
  125. package/src/modules-repository/theme/theme-service.ts +389 -0
  126. package/src/modules-repository/theme/themes/theme-cherry-blossom.css +78 -0
  127. package/src/modules-repository/theme/themes/theme-cozy.css +111 -0
  128. package/src/modules-repository/theme/themes/theme-cyberpunk.css +150 -0
  129. package/src/modules-repository/theme/themes/theme-dark.css +79 -0
  130. package/src/modules-repository/theme/themes/theme-forest.css +171 -0
  131. package/src/modules-repository/theme/themes/theme-gold.css +100 -0
  132. package/src/modules-repository/theme/themes/theme-high-contrast.css +126 -0
  133. package/src/modules-repository/theme/themes/theme-lava.css +101 -0
  134. package/src/modules-repository/theme/themes/theme-lavender.css +90 -0
  135. package/src/modules-repository/theme/themes/theme-light.css +79 -0
  136. package/src/modules-repository/theme/themes/theme-matrix.css +103 -0
  137. package/src/modules-repository/theme/themes/theme-midnight.css +81 -0
  138. package/src/modules-repository/theme/themes/theme-nord.css +94 -0
  139. package/src/modules-repository/theme/themes/theme-ocean.css +84 -0
  140. package/src/modules-repository/theme/themes/theme-retro80s.css +343 -0
  141. package/src/modules-repository/theme/themes/theme-sunset.css +62 -0
  142. package/src/modules-repository/theme/themes-config.d.ts +27 -0
  143. package/src/modules-repository/theme/themes-config.json +213 -0
  144. package/src/vite-env.d.ts +1 -0
  145. package/tsconfig.json +33 -0
  146. package/vite.config.ts +99 -0
@@ -0,0 +1,435 @@
1
+ # KIMU API Core Module
2
+
3
+ Modulo minimale per la gestione di richieste HTTP verso API esterne, basato su `fetch` nativo.
4
+
5
+ ## Caratteristiche
6
+
7
+ - ✅ Zero dipendenze esterne
8
+ - ✅ Basato su fetch API nativa
9
+ - ✅ Promise-based (async/await)
10
+ - ✅ TypeScript-first con type safety completo
11
+ - ✅ Supporto per tutti i metodi HTTP (GET, POST, PUT, PATCH, DELETE)
12
+ - ✅ Gestione automatica di query parameters, headers, body
13
+ - ✅ Interceptor per request/response/error
14
+ - ✅ Timeout configurabile
15
+ - ✅ Gestione errori strutturata
16
+ - ✅ Supporto per diversi tipi di risposta (JSON, text, blob, arraybuffer)
17
+ - ✅ BaseURL globale configurabile
18
+ - ✅ Lightweight (~2KB minified)
19
+
20
+ ## Installazione
21
+
22
+ Il modulo è incluso nel repository `kimu-modules`. Per installarlo:
23
+
24
+ ```bash
25
+ # Usando kimu-cli (quando disponibile)
26
+ kimu install module api-core
27
+
28
+ # Manualmente
29
+ # Copia la cartella src/modules/api-core nel tuo progetto
30
+ ```
31
+
32
+ ## Utilizzo Base
33
+
34
+ ### Inizializzazione
35
+
36
+ ```typescript
37
+ import { KimuModuleManager } from '@kimu/core';
38
+ import ApiCoreModule from './modules/api-core/module';
39
+
40
+ // Inizializza il modulo
41
+ const moduleManager = KimuModuleManager.getInstance();
42
+ const apiModule = new ApiCoreModule();
43
+ await moduleManager.loadModule(apiModule);
44
+
45
+ // Ottieni il servizio
46
+ const api = moduleManager.getService('api-core');
47
+ ```
48
+
49
+ ### Configurazione Globale
50
+
51
+ ```typescript
52
+ import { apiCoreService } from './modules/api-core/api-core-service';
53
+
54
+ // Configura baseURL e headers di default
55
+ apiCoreService.configure({
56
+ baseURL: 'https://api.example.com',
57
+ timeout: 10000, // 10 secondi
58
+ headers: {
59
+ 'Authorization': 'Bearer YOUR_TOKEN',
60
+ 'X-Custom-Header': 'value'
61
+ }
62
+ });
63
+ ```
64
+
65
+ ### Richieste GET
66
+
67
+ ```typescript
68
+ // GET semplice
69
+ const response = await api.get('/users');
70
+ console.log(response.data); // Array di utenti
71
+
72
+ // GET con query parameters
73
+ const response = await api.get('/users', {
74
+ params: {
75
+ page: 1,
76
+ limit: 10,
77
+ sort: 'name'
78
+ }
79
+ });
80
+
81
+ // GET con headers custom
82
+ const response = await api.get('/users', {
83
+ headers: {
84
+ 'Accept-Language': 'it-IT'
85
+ }
86
+ });
87
+ ```
88
+
89
+ ### Richieste POST
90
+
91
+ ```typescript
92
+ // POST con JSON body
93
+ const response = await api.post('/users', {
94
+ name: 'Mario Rossi',
95
+ email: 'mario@example.com',
96
+ age: 30
97
+ });
98
+
99
+ // POST con FormData
100
+ const formData = new FormData();
101
+ formData.append('file', fileInput.files[0]);
102
+ formData.append('title', 'My File');
103
+
104
+ const response = await api.post('/upload', formData);
105
+ ```
106
+
107
+ ### Altri Metodi HTTP
108
+
109
+ ```typescript
110
+ // PUT - aggiorna risorsa
111
+ const response = await api.put('/users/123', {
112
+ name: 'Mario Rossi Updated'
113
+ });
114
+
115
+ // PATCH - aggiorna parzialmente
116
+ const response = await api.patch('/users/123', {
117
+ age: 31
118
+ });
119
+
120
+ // DELETE - elimina risorsa
121
+ const response = await api.delete('/users/123');
122
+ ```
123
+
124
+ ### Richiesta Generica
125
+
126
+ ```typescript
127
+ const response = await api.request('/custom-endpoint', {
128
+ method: 'POST',
129
+ body: { key: 'value' },
130
+ headers: { 'X-Custom': 'header' },
131
+ params: { filter: 'active' },
132
+ timeout: 5000,
133
+ responseType: 'json'
134
+ });
135
+ ```
136
+
137
+ ## Gestione Errori
138
+
139
+ ```typescript
140
+ try {
141
+ const response = await api.get('/users');
142
+ console.log(response.data);
143
+ } catch (error) {
144
+ console.error('Error:', error.message);
145
+ console.error('Status:', error.status);
146
+ console.error('Status Text:', error.statusText);
147
+ console.error('Response Data:', error.data);
148
+ }
149
+ ```
150
+
151
+ ## Interceptor
152
+
153
+ ### Request Interceptor
154
+
155
+ ```typescript
156
+ apiCoreService.configure({
157
+ requestInterceptor: (config) => {
158
+ // Aggiungi token dinamicamente
159
+ const token = localStorage.getItem('token');
160
+ if (token) {
161
+ config.headers = {
162
+ ...config.headers,
163
+ 'Authorization': `Bearer ${token}`
164
+ };
165
+ }
166
+
167
+ // Log della richiesta
168
+ console.log('Making request:', config);
169
+
170
+ return config;
171
+ }
172
+ });
173
+ ```
174
+
175
+ ### Response Interceptor
176
+
177
+ ```typescript
178
+ apiCoreService.configure({
179
+ responseInterceptor: (response) => {
180
+ // Log della risposta
181
+ console.log('Response received:', response.status);
182
+
183
+ // Trasforma i dati
184
+ response.data = {
185
+ ...response.data,
186
+ receivedAt: new Date().toISOString()
187
+ };
188
+
189
+ return response;
190
+ }
191
+ });
192
+ ```
193
+
194
+ ### Error Interceptor
195
+
196
+ ```typescript
197
+ apiCoreService.configure({
198
+ errorInterceptor: async (error) => {
199
+ // Gestione errore 401: refresh token
200
+ if (error.status === 401) {
201
+ const newToken = await refreshToken();
202
+ localStorage.setItem('token', newToken);
203
+
204
+ // Puoi anche rilanciare una richiesta o modificare l'errore
205
+ error.message = 'Session expired, please login again';
206
+ }
207
+
208
+ // Log errori
209
+ console.error('API Error:', error);
210
+
211
+ return error;
212
+ }
213
+ });
214
+ ```
215
+
216
+ ## Timeout e Cancellazione
217
+
218
+ ```typescript
219
+ // Timeout automatico
220
+ const response = await api.get('/slow-endpoint', {
221
+ timeout: 3000 // 3 secondi
222
+ });
223
+
224
+ // Cancellazione manuale con AbortController
225
+ const controller = new AbortController();
226
+
227
+ const request = api.get('/data', {
228
+ signal: controller.signal
229
+ });
230
+
231
+ // Cancella dopo 2 secondi
232
+ setTimeout(() => controller.abort(), 2000);
233
+
234
+ try {
235
+ const response = await request;
236
+ } catch (error) {
237
+ if (error.message.includes('timeout')) {
238
+ console.log('Request was cancelled');
239
+ }
240
+ }
241
+ ```
242
+
243
+ ## Tipi di Risposta
244
+
245
+ ```typescript
246
+ // JSON (default)
247
+ const response = await api.get('/users', {
248
+ responseType: 'json'
249
+ });
250
+
251
+ // Testo
252
+ const response = await api.get('/readme.txt', {
253
+ responseType: 'text'
254
+ });
255
+ console.log(response.data); // stringa
256
+
257
+ // Blob (per file)
258
+ const response = await api.get('/image.png', {
259
+ responseType: 'blob'
260
+ });
261
+ const imageUrl = URL.createObjectURL(response.data);
262
+
263
+ // ArrayBuffer (per dati binari)
264
+ const response = await api.get('/binary-data', {
265
+ responseType: 'arraybuffer'
266
+ });
267
+ ```
268
+
269
+ ## Uso in Componenti KIMU
270
+
271
+ ```typescript
272
+ import { KimuComponentElement } from '@kimu/core';
273
+ import { KimuModuleManager } from '@kimu/core';
274
+
275
+ export class UserListComponent extends KimuComponentElement {
276
+ private api: any;
277
+ private users: any[] = [];
278
+
279
+ async onInit() {
280
+ // Ottieni il servizio API
281
+ this.api = KimuModuleManager.getInstance().getService('api-core');
282
+
283
+ // Carica i dati
284
+ await this.loadUsers();
285
+ }
286
+
287
+ async loadUsers() {
288
+ try {
289
+ const response = await this.api.get('/users');
290
+ this.users = response.data;
291
+ this.onRender();
292
+ } catch (error) {
293
+ console.error('Failed to load users:', error);
294
+ }
295
+ }
296
+
297
+ async createUser(userData: any) {
298
+ try {
299
+ const response = await this.api.post('/users', userData);
300
+ this.users.push(response.data);
301
+ this.onRender();
302
+ } catch (error) {
303
+ console.error('Failed to create user:', error);
304
+ }
305
+ }
306
+ }
307
+ ```
308
+
309
+ ## TypeScript Support
310
+
311
+ Il modulo è completamente tipizzato:
312
+
313
+ ```typescript
314
+ interface User {
315
+ id: number;
316
+ name: string;
317
+ email: string;
318
+ }
319
+
320
+ // Type-safe request
321
+ const response = await api.get<User[]>('/users');
322
+ response.data.forEach(user => {
323
+ console.log(user.name); // Type-safe access
324
+ });
325
+
326
+ // Type-safe POST
327
+ const newUser: Omit<User, 'id'> = {
328
+ name: 'Mario',
329
+ email: 'mario@example.com'
330
+ };
331
+
332
+ const response = await api.post<User>('/users', newUser);
333
+ console.log(response.data.id); // Type-safe
334
+ ```
335
+
336
+ ## Best Practices
337
+
338
+ ### 1. Configura baseURL globalmente
339
+ ```typescript
340
+ apiCoreService.configure({
341
+ baseURL: process.env.API_URL || 'https://api.example.com'
342
+ });
343
+ ```
344
+
345
+ ### 2. Usa interceptor per autenticazione
346
+ ```typescript
347
+ apiCoreService.configure({
348
+ requestInterceptor: (config) => {
349
+ const token = getAuthToken();
350
+ if (token) {
351
+ config.headers = {
352
+ ...config.headers,
353
+ 'Authorization': `Bearer ${token}`
354
+ };
355
+ }
356
+ return config;
357
+ }
358
+ });
359
+ ```
360
+
361
+ ### 3. Gestisci errori globalmente
362
+ ```typescript
363
+ apiCoreService.configure({
364
+ errorInterceptor: (error) => {
365
+ if (error.status === 401) {
366
+ // Redirect to login
367
+ window.location.href = '/login';
368
+ } else if (error.status >= 500) {
369
+ // Show error notification
370
+ showNotification('Server error, please try again later');
371
+ }
372
+ return error;
373
+ }
374
+ });
375
+ ```
376
+
377
+ ### 4. Crea wrapper service per API specifiche
378
+ ```typescript
379
+ class UserService {
380
+ private api = apiCoreService;
381
+
382
+ async getUsers() {
383
+ return this.api.get<User[]>('/users');
384
+ }
385
+
386
+ async getUser(id: number) {
387
+ return this.api.get<User>(`/users/${id}`);
388
+ }
389
+
390
+ async createUser(user: Omit<User, 'id'>) {
391
+ return this.api.post<User>('/users', user);
392
+ }
393
+
394
+ async updateUser(id: number, user: Partial<User>) {
395
+ return this.api.patch<User>(`/users/${id}`, user);
396
+ }
397
+
398
+ async deleteUser(id: number) {
399
+ return this.api.delete(`/users/${id}`);
400
+ }
401
+ }
402
+ ```
403
+
404
+ ## Confronto con Altri Framework
405
+
406
+ | Feature | KIMU api-core | Angular HttpClient | Axios | Fetch nativo |
407
+ |---------|---------------|-------------------|-------|--------------|
408
+ | Bundle Size | ~2KB | ~40KB | ~13KB | 0KB (nativo) |
409
+ | Dependencies | 0 | RxJS required | 0 | 0 |
410
+ | TypeScript | ✅ Native | ✅ Native | ✅ Native | ⚠️ Basic |
411
+ | Interceptors | ✅ | ✅ | ✅ | ❌ |
412
+ | Timeout | ✅ | ✅ | ✅ | ⚠️ Manual |
413
+ | Query Params | ✅ Auto | ✅ Auto | ✅ Auto | ❌ Manual |
414
+ | Error Handling | ✅ Structured | ✅ Structured | ✅ Structured | ⚠️ Basic |
415
+ | Promise/Observable | Promise | Observable | Promise | Promise |
416
+
417
+ ## Roadmap
418
+
419
+ - [ ] Support per retry automatico
420
+ - [ ] Caching integrato delle richieste
421
+ - [ ] Request deduplication
422
+ - [ ] Upload progress tracking
423
+ - [ ] Download progress tracking
424
+ - [ ] Request queue con priorità
425
+ - [ ] Mock/test utilities
426
+
427
+ ## License
428
+
429
+ Questo modulo fa parte di KIMU-Core ed è distribuito sotto licenza MPL-2.0.
430
+
431
+ ## Supporto
432
+
433
+ Per issue, feature request o domande:
434
+ - GitHub Issues: [kimu-core/issues](https://github.com/UnicoVerso/kimu-core/issues)
435
+ - Documentazione: [kimu-docs](https://github.com/UnicoVerso/kimu-docs)
@@ -0,0 +1,289 @@
1
+ /**
2
+ * KIMU API Core Service
3
+ * Lightweight HTTP client based on native fetch API
4
+ * Zero dependencies, Promise-based, TypeScript-first
5
+ */
6
+
7
+ import {
8
+ ApiRequestOptions,
9
+ ApiResponse,
10
+ ApiError,
11
+ ApiConfig
12
+ } from './interfaces';
13
+
14
+ /**
15
+ * Main API service class for making HTTP requests
16
+ */
17
+ export class ApiCoreService {
18
+ private config: ApiConfig = {};
19
+
20
+ /**
21
+ * Configure the API service with global settings
22
+ * @param config - Global configuration options
23
+ */
24
+ configure(config: ApiConfig): void {
25
+ this.config = { ...this.config, ...config };
26
+ }
27
+
28
+ /**
29
+ * Make a GET request
30
+ * @param url - Request URL (relative to baseURL if configured)
31
+ * @param options - Request options
32
+ * @returns Promise with response data
33
+ */
34
+ async get<T = any>(url: string, options?: ApiRequestOptions): Promise<ApiResponse<T>> {
35
+ return this.request<T>(url, { ...options, method: 'GET' });
36
+ }
37
+
38
+ /**
39
+ * Make a POST request
40
+ * @param url - Request URL
41
+ * @param data - Request body data
42
+ * @param options - Request options
43
+ * @returns Promise with response data
44
+ */
45
+ async post<T = any>(url: string, data?: any, options?: ApiRequestOptions): Promise<ApiResponse<T>> {
46
+ return this.request<T>(url, { ...options, method: 'POST', body: data });
47
+ }
48
+
49
+ /**
50
+ * Make a PUT request
51
+ * @param url - Request URL
52
+ * @param data - Request body data
53
+ * @param options - Request options
54
+ * @returns Promise with response data
55
+ */
56
+ async put<T = any>(url: string, data?: any, options?: ApiRequestOptions): Promise<ApiResponse<T>> {
57
+ return this.request<T>(url, { ...options, method: 'PUT', body: data });
58
+ }
59
+
60
+ /**
61
+ * Make a PATCH request
62
+ * @param url - Request URL
63
+ * @param data - Request body data
64
+ * @param options - Request options
65
+ * @returns Promise with response data
66
+ */
67
+ async patch<T = any>(url: string, data?: any, options?: ApiRequestOptions): Promise<ApiResponse<T>> {
68
+ return this.request<T>(url, { ...options, method: 'PATCH', body: data });
69
+ }
70
+
71
+ /**
72
+ * Make a DELETE request
73
+ * @param url - Request URL
74
+ * @param options - Request options
75
+ * @returns Promise with response data
76
+ */
77
+ async delete<T = any>(url: string, options?: ApiRequestOptions): Promise<ApiResponse<T>> {
78
+ return this.request<T>(url, { ...options, method: 'DELETE' });
79
+ }
80
+
81
+ /**
82
+ * Make a generic HTTP request
83
+ * @param url - Request URL
84
+ * @param options - Request options
85
+ * @returns Promise with response data
86
+ */
87
+ async request<T = any>(url: string, options: ApiRequestOptions = {}): Promise<ApiResponse<T>> {
88
+ try {
89
+ // Apply request interceptor if configured
90
+ let finalOptions = options;
91
+ if (this.config.requestInterceptor) {
92
+ finalOptions = await this.config.requestInterceptor(options);
93
+ }
94
+
95
+ // Build final URL with baseURL and query params
96
+ const finalUrl = this.buildUrl(url, finalOptions.params);
97
+
98
+ // Build headers
99
+ const headers = this.buildHeaders(finalOptions);
100
+
101
+ // Build request body
102
+ const body = this.buildBody(finalOptions.body, headers);
103
+
104
+ // Setup timeout if specified
105
+ const controller = new AbortController();
106
+ const signal = finalOptions.signal || controller.signal;
107
+ const timeout = finalOptions.timeout || this.config.timeout;
108
+
109
+ let timeoutId: NodeJS.Timeout | undefined;
110
+ if (timeout) {
111
+ timeoutId = setTimeout(() => controller.abort(), timeout);
112
+ }
113
+
114
+ // Make fetch request
115
+ const response = await fetch(finalUrl, {
116
+ method: finalOptions.method || 'GET',
117
+ headers,
118
+ body,
119
+ credentials: finalOptions.credentials,
120
+ cache: finalOptions.cache,
121
+ signal
122
+ });
123
+
124
+ // Clear timeout
125
+ if (timeoutId) {
126
+ clearTimeout(timeoutId);
127
+ }
128
+
129
+ // Parse response
130
+ const responseData = await this.parseResponse<T>(response, finalOptions.responseType);
131
+
132
+ // Build API response object
133
+ let apiResponse: ApiResponse<T> = {
134
+ data: responseData,
135
+ status: response.status,
136
+ statusText: response.statusText,
137
+ headers: response.headers,
138
+ raw: response
139
+ };
140
+
141
+ // Apply response interceptor if configured
142
+ if (this.config.responseInterceptor) {
143
+ apiResponse = await this.config.responseInterceptor(apiResponse);
144
+ }
145
+
146
+ // Check if response is ok
147
+ if (!response.ok) {
148
+ throw this.createError(
149
+ `HTTP Error ${response.status}: ${response.statusText}`,
150
+ response.status,
151
+ response.statusText,
152
+ responseData
153
+ );
154
+ }
155
+
156
+ return apiResponse;
157
+
158
+ } catch (error: any) {
159
+ // Create API error
160
+ let apiError: ApiError;
161
+
162
+ if (error.name === 'AbortError') {
163
+ apiError = this.createError('Request timeout', undefined, undefined, undefined, error);
164
+ } else if (error.message?.includes('HTTP Error')) {
165
+ apiError = error;
166
+ } else {
167
+ apiError = this.createError(
168
+ error.message || 'Network error',
169
+ undefined,
170
+ undefined,
171
+ undefined,
172
+ error
173
+ );
174
+ }
175
+
176
+ // Apply error interceptor if configured
177
+ if (this.config.errorInterceptor) {
178
+ apiError = await this.config.errorInterceptor(apiError);
179
+ }
180
+
181
+ throw apiError;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Build final URL with baseURL and query parameters
187
+ */
188
+ private buildUrl(url: string, params?: Record<string, string | number | boolean>): string {
189
+ let finalUrl = url;
190
+
191
+ // Add baseURL if configured and URL is relative
192
+ if (this.config.baseURL && !url.startsWith('http')) {
193
+ finalUrl = `${this.config.baseURL.replace(/\/$/, '')}/${url.replace(/^\//, '')}`;
194
+ }
195
+
196
+ // Add query parameters
197
+ if (params) {
198
+ const queryString = Object.entries(params)
199
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
200
+ .join('&');
201
+
202
+ finalUrl += (finalUrl.includes('?') ? '&' : '?') + queryString;
203
+ }
204
+
205
+ return finalUrl;
206
+ }
207
+
208
+ /**
209
+ * Build request headers
210
+ */
211
+ private buildHeaders(options: ApiRequestOptions): HeadersInit {
212
+ const headers: Record<string, string> = {
213
+ ...this.config.headers,
214
+ ...options.headers
215
+ };
216
+
217
+ // Auto-add Content-Type for JSON body if not specified
218
+ if (options.body && typeof options.body === 'object' && !headers['Content-Type']) {
219
+ headers['Content-Type'] = 'application/json';
220
+ }
221
+
222
+ return headers;
223
+ }
224
+
225
+ /**
226
+ * Build request body
227
+ */
228
+ private buildBody(body: any, _headers: HeadersInit): BodyInit | undefined {
229
+ if (!body) {
230
+ return undefined;
231
+ }
232
+
233
+ // If body is already a string, FormData, Blob, etc., return as-is
234
+ if (typeof body === 'string' || body instanceof FormData || body instanceof Blob) {
235
+ return body;
236
+ }
237
+
238
+ // Otherwise, stringify as JSON
239
+ return JSON.stringify(body);
240
+ }
241
+
242
+ /**
243
+ * Parse response based on response type
244
+ */
245
+ private async parseResponse<T>(response: Response, responseType?: string): Promise<T> {
246
+ const type = responseType || 'json';
247
+
248
+ switch (type) {
249
+ case 'json':
250
+ try {
251
+ return await response.json();
252
+ } catch {
253
+ return null as T;
254
+ }
255
+ case 'text':
256
+ return await response.text() as T;
257
+ case 'blob':
258
+ return await response.blob() as T;
259
+ case 'arraybuffer':
260
+ return await response.arrayBuffer() as T;
261
+ default:
262
+ return await response.json();
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Create an API error object
268
+ */
269
+ private createError(
270
+ message: string,
271
+ status?: number,
272
+ statusText?: string,
273
+ data?: any,
274
+ originalError?: Error
275
+ ): ApiError {
276
+ return {
277
+ message,
278
+ status,
279
+ statusText,
280
+ data,
281
+ originalError: originalError || new Error(message)
282
+ };
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Singleton instance of API Core Service
288
+ */
289
+ export const apiCoreService = new ApiCoreService();