@blackcube/aurelia2-bleet 1.0.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.
Files changed (157) hide show
  1. package/blackcube-aurelia2-bleet-1.0.0.tgz +0 -0
  2. package/dist/index.es.js +4514 -0
  3. package/dist/index.es.js.map +1 -0
  4. package/dist/index.js +4549 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/types/attributes/ajaxify-trigger.d.ts +36 -0
  7. package/dist/types/attributes/ajaxify-trigger.d.ts.map +1 -0
  8. package/dist/types/attributes/alert.d.ts +15 -0
  9. package/dist/types/attributes/alert.d.ts.map +1 -0
  10. package/dist/types/attributes/badge.d.ts +13 -0
  11. package/dist/types/attributes/badge.d.ts.map +1 -0
  12. package/dist/types/attributes/burger.d.ts +11 -0
  13. package/dist/types/attributes/burger.d.ts.map +1 -0
  14. package/dist/types/attributes/drawer-trigger.d.ts +16 -0
  15. package/dist/types/attributes/drawer-trigger.d.ts.map +1 -0
  16. package/dist/types/attributes/dropdown.d.ts +38 -0
  17. package/dist/types/attributes/dropdown.d.ts.map +1 -0
  18. package/dist/types/attributes/index.d.ts +16 -0
  19. package/dist/types/attributes/index.d.ts.map +1 -0
  20. package/dist/types/attributes/menu.d.ts +32 -0
  21. package/dist/types/attributes/menu.d.ts.map +1 -0
  22. package/dist/types/attributes/modal-trigger.d.ts +16 -0
  23. package/dist/types/attributes/modal-trigger.d.ts.map +1 -0
  24. package/dist/types/attributes/pager.d.ts +13 -0
  25. package/dist/types/attributes/pager.d.ts.map +1 -0
  26. package/dist/types/attributes/password.d.ts +15 -0
  27. package/dist/types/attributes/password.d.ts.map +1 -0
  28. package/dist/types/attributes/profile.d.ts +24 -0
  29. package/dist/types/attributes/profile.d.ts.map +1 -0
  30. package/dist/types/attributes/select.d.ts +24 -0
  31. package/dist/types/attributes/select.d.ts.map +1 -0
  32. package/dist/types/attributes/tabs.d.ts +16 -0
  33. package/dist/types/attributes/tabs.d.ts.map +1 -0
  34. package/dist/types/attributes/toaster-trigger.d.ts +19 -0
  35. package/dist/types/attributes/toaster-trigger.d.ts.map +1 -0
  36. package/dist/types/attributes/upload.d.ts +57 -0
  37. package/dist/types/attributes/upload.d.ts.map +1 -0
  38. package/dist/types/codecs/ajaxify-codec.d.ts +5 -0
  39. package/dist/types/codecs/ajaxify-codec.d.ts.map +1 -0
  40. package/dist/types/codecs/csrf-codec.d.ts +7 -0
  41. package/dist/types/codecs/csrf-codec.d.ts.map +1 -0
  42. package/dist/types/codecs/request-codec.d.ts +5 -0
  43. package/dist/types/codecs/request-codec.d.ts.map +1 -0
  44. package/dist/types/components/bleet-ajaxify.d.ts +17 -0
  45. package/dist/types/components/bleet-ajaxify.d.ts.map +1 -0
  46. package/dist/types/components/bleet-ajaxify.html.d.ts +3 -0
  47. package/dist/types/components/bleet-ajaxify.html.d.ts.map +1 -0
  48. package/dist/types/components/bleet-drawer.d.ts +40 -0
  49. package/dist/types/components/bleet-drawer.d.ts.map +1 -0
  50. package/dist/types/components/bleet-drawer.html.d.ts +3 -0
  51. package/dist/types/components/bleet-drawer.html.d.ts.map +1 -0
  52. package/dist/types/components/bleet-modal.d.ts +46 -0
  53. package/dist/types/components/bleet-modal.d.ts.map +1 -0
  54. package/dist/types/components/bleet-modal.html.d.ts +3 -0
  55. package/dist/types/components/bleet-modal.html.d.ts.map +1 -0
  56. package/dist/types/components/bleet-overlay.d.ts +21 -0
  57. package/dist/types/components/bleet-overlay.d.ts.map +1 -0
  58. package/dist/types/components/bleet-quilljs.d.ts +19 -0
  59. package/dist/types/components/bleet-quilljs.d.ts.map +1 -0
  60. package/dist/types/components/bleet-quilljs.html.d.ts +3 -0
  61. package/dist/types/components/bleet-quilljs.html.d.ts.map +1 -0
  62. package/dist/types/components/bleet-toast.d.ts +26 -0
  63. package/dist/types/components/bleet-toast.d.ts.map +1 -0
  64. package/dist/types/components/bleet-toast.html.d.ts +3 -0
  65. package/dist/types/components/bleet-toast.html.d.ts.map +1 -0
  66. package/dist/types/components/bleet-toaster-trigger.d.ts +20 -0
  67. package/dist/types/components/bleet-toaster-trigger.d.ts.map +1 -0
  68. package/dist/types/components/bleet-toaster.d.ts +15 -0
  69. package/dist/types/components/bleet-toaster.d.ts.map +1 -0
  70. package/dist/types/components/bleet-toaster.html.d.ts +3 -0
  71. package/dist/types/components/bleet-toaster.html.d.ts.map +1 -0
  72. package/dist/types/components/index.d.ts +9 -0
  73. package/dist/types/components/index.d.ts.map +1 -0
  74. package/dist/types/configure.d.ts +35 -0
  75. package/dist/types/configure.d.ts.map +1 -0
  76. package/dist/types/enums/api.d.ts +11 -0
  77. package/dist/types/enums/api.d.ts.map +1 -0
  78. package/dist/types/enums/event-aggregator.d.ts +123 -0
  79. package/dist/types/enums/event-aggregator.d.ts.map +1 -0
  80. package/dist/types/index.d.ts +26 -0
  81. package/dist/types/index.d.ts.map +1 -0
  82. package/dist/types/interfaces/api.d.ts +56 -0
  83. package/dist/types/interfaces/api.d.ts.map +1 -0
  84. package/dist/types/interfaces/dialog.d.ts +18 -0
  85. package/dist/types/interfaces/dialog.d.ts.map +1 -0
  86. package/dist/types/interfaces/event-aggregator.d.ts +75 -0
  87. package/dist/types/interfaces/event-aggregator.d.ts.map +1 -0
  88. package/dist/types/services/api-service.d.ts +64 -0
  89. package/dist/types/services/api-service.d.ts.map +1 -0
  90. package/dist/types/services/http-service.d.ts +22 -0
  91. package/dist/types/services/http-service.d.ts.map +1 -0
  92. package/dist/types/services/socketio-service.d.ts +23 -0
  93. package/dist/types/services/socketio-service.d.ts.map +1 -0
  94. package/dist/types/services/storage-service.d.ts +13 -0
  95. package/dist/types/services/storage-service.d.ts.map +1 -0
  96. package/dist/types/services/svg-service.d.ts +17 -0
  97. package/dist/types/services/svg-service.d.ts.map +1 -0
  98. package/dist/types/services/transition-service.d.ts +13 -0
  99. package/dist/types/services/transition-service.d.ts.map +1 -0
  100. package/dist/types/services/trap-focus-service.d.ts +28 -0
  101. package/dist/types/services/trap-focus-service.d.ts.map +1 -0
  102. package/doc/bleet-api-reference.md +1333 -0
  103. package/doc/bleet-model-api-reference.md +379 -0
  104. package/doc/bleet-typescript-api-reference.md +1037 -0
  105. package/package.json +43 -0
  106. package/resource.d.ts +22 -0
  107. package/src/attributes/ajaxify-trigger.ts +218 -0
  108. package/src/attributes/alert.ts +55 -0
  109. package/src/attributes/badge.ts +39 -0
  110. package/src/attributes/burger.ts +36 -0
  111. package/src/attributes/drawer-trigger.ts +53 -0
  112. package/src/attributes/dropdown.ts +377 -0
  113. package/src/attributes/index.ts +15 -0
  114. package/src/attributes/menu.ts +179 -0
  115. package/src/attributes/modal-trigger.ts +53 -0
  116. package/src/attributes/pager.ts +43 -0
  117. package/src/attributes/password.ts +47 -0
  118. package/src/attributes/profile.ts +112 -0
  119. package/src/attributes/select.ts +214 -0
  120. package/src/attributes/tabs.ts +99 -0
  121. package/src/attributes/toaster-trigger.ts +54 -0
  122. package/src/attributes/upload.ts +380 -0
  123. package/src/codecs/ajaxify-codec.ts +16 -0
  124. package/src/codecs/csrf-codec.ts +41 -0
  125. package/src/codecs/request-codec.ts +16 -0
  126. package/src/components/bleet-ajaxify.html.ts +4 -0
  127. package/src/components/bleet-ajaxify.ts +62 -0
  128. package/src/components/bleet-drawer.html.ts +36 -0
  129. package/src/components/bleet-drawer.ts +236 -0
  130. package/src/components/bleet-modal.html.ts +30 -0
  131. package/src/components/bleet-modal.ts +274 -0
  132. package/src/components/bleet-overlay.ts +111 -0
  133. package/src/components/bleet-quilljs.html.ts +4 -0
  134. package/src/components/bleet-quilljs.ts +73 -0
  135. package/src/components/bleet-toast.html.ts +44 -0
  136. package/src/components/bleet-toast.ts +133 -0
  137. package/src/components/bleet-toaster-trigger.ts +66 -0
  138. package/src/components/bleet-toaster.html.ts +11 -0
  139. package/src/components/bleet-toaster.ts +72 -0
  140. package/src/components/index.ts +8 -0
  141. package/src/configure.ts +121 -0
  142. package/src/enums/api.ts +12 -0
  143. package/src/enums/event-aggregator.ts +131 -0
  144. package/src/index.ts +220 -0
  145. package/src/interfaces/api.ts +64 -0
  146. package/src/interfaces/dialog.ts +25 -0
  147. package/src/interfaces/event-aggregator.ts +88 -0
  148. package/src/services/api-service.ts +387 -0
  149. package/src/services/http-service.ts +166 -0
  150. package/src/services/socketio-service.ts +138 -0
  151. package/src/services/storage-service.ts +36 -0
  152. package/src/services/svg-service.ts +35 -0
  153. package/src/services/transition-service.ts +39 -0
  154. package/src/services/trap-focus-service.ts +213 -0
  155. package/src/types/css.d.ts +4 -0
  156. package/src/types/html.d.ts +12 -0
  157. package/src/types/svg.d.ts +4 -0
@@ -0,0 +1,387 @@
1
+ import {DI, ILogger, resolve} from 'aurelia';
2
+ import {
3
+ IBuiltRequest,
4
+ ICacheConfig,
5
+ ICacheEntry,
6
+ ICodec,
7
+ IHttpRequest,
8
+ IHttpResponse,
9
+ IPaginationConfig,
10
+ ITransport
11
+ } from '../interfaces/api';
12
+ import {IBleetConfiguration} from '../configure';
13
+ import {CsrfCodec} from '../codecs/csrf-codec';
14
+
15
+ export interface IApiService extends ApiService {}
16
+ export const IApiService = /*@__PURE__*/DI.createInterface<IApiService>(
17
+ 'IApiService',
18
+ (x) => x.singleton(ApiService)
19
+ );
20
+
21
+ export class ApiService {
22
+ private static readonly PARAM_PATTERN = /:[a-zA-Z_][a-zA-Z0-9_-]*/g;
23
+ private memoryCache = new Map<string, ICacheEntry>();
24
+
25
+ public constructor(
26
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('ApiService'),
27
+ private readonly config: IBleetConfiguration = resolve(IBleetConfiguration),
28
+ ) {
29
+ this.logger.trace('constructor');
30
+ }
31
+
32
+ public url(path: string, params?: Record<string, any>): ApiRequestBuilder {
33
+ return new ApiRequestBuilder(this, path, params);
34
+ }
35
+
36
+ /**
37
+ * Simple HTML fetch for AJAX dialogs (modal/drawer)
38
+ * Returns full response for status code checking
39
+ */
40
+ public async fetchHtml(url: string): Promise<IHttpResponse<string>> {
41
+ this.logger.trace('fetchHtml', url);
42
+ return this.url(url).toText().get<string>();
43
+ }
44
+
45
+ public execute<T>(request: IBuiltRequest): Promise<IHttpResponse<T>> {
46
+ this.logger.trace('execute', request.method, request.url);
47
+
48
+ // 1. Validate path params are present in pathParams or data
49
+ this.validateParams(request.url, request.pathParams, request.data);
50
+
51
+ // 2. Build request context
52
+ const initialCtx: IHttpRequest = {
53
+ url: request.url,
54
+ method: request.method,
55
+ headers: request.headers,
56
+ data: request.data,
57
+ pathParams: request.pathParams
58
+ };
59
+
60
+ // 3. Build codec chain : CSRF auto + user codecs
61
+ const csrfConfig = this.config.getCsrfConfig();
62
+ const allInputCodecs: ICodec[] = [];
63
+ if (csrfConfig.enabled) {
64
+ allInputCodecs.push(CsrfCodec.fromConfig(csrfConfig));
65
+ }
66
+ allInputCodecs.push(...request.inputCodecs);
67
+
68
+ // 4. Apply input codecs (encode) — chaîne de promesses
69
+ const ctxPromise = allInputCodecs.reduce(
70
+ (promise, codec) => codec.encode ? promise.then((ctx) => codec.encode!(ctx)) : promise,
71
+ Promise.resolve(initialCtx)
72
+ );
73
+
74
+ return ctxPromise.then((ctx) => {
75
+ // 4. Check cache
76
+ if (request.cache) {
77
+ const cached = this.getFromCache<T>(request, ctx);
78
+ if (cached) {
79
+ this.logger.trace('execute:cache-hit');
80
+ return Promise.resolve(cached);
81
+ }
82
+ }
83
+
84
+ // 5. Execute via transport avec fallback
85
+ return this.executeWithFallback<T>(ctx, request.responseType)
86
+ .then((response) => {
87
+ // 6. Apply output codecs (decode)
88
+ return request.outputCodecs.reduce(
89
+ (promise, codec) => codec.decode ? promise.then((r) => codec.decode!(r)) : promise,
90
+ Promise.resolve(response)
91
+ );
92
+ })
93
+ .then((response) => {
94
+ // 7. Store in cache
95
+ if (request.cache) {
96
+ this.storeInCache(request, ctx, response);
97
+ }
98
+ return response;
99
+ });
100
+ });
101
+ }
102
+
103
+ private executeWithFallback<T>(
104
+ ctx: IHttpRequest,
105
+ responseType: 'json' | 'text' | 'blob' | 'arraybuffer' | 'auto'
106
+ ): Promise<IHttpResponse<T>> {
107
+ const transports = this.config.getAvailableTransports();
108
+
109
+ if (transports.length === 0) {
110
+ return Promise.reject(new Error('No transport available'));
111
+ }
112
+
113
+ const tryTransport = (index: number, lastError: Error | null): Promise<IHttpResponse<T>> => {
114
+ if (index >= transports.length) {
115
+ return Promise.reject(lastError ?? new Error('All transports failed'));
116
+ }
117
+
118
+ const transport: ITransport = transports[index];
119
+
120
+ if (!transport.isAvailable()) {
121
+ this.logger.trace('execute:transport-unavailable', transport.type);
122
+ return tryTransport(index + 1, lastError);
123
+ }
124
+
125
+ this.logger.trace('execute:trying', transport.type);
126
+
127
+ const preparedCtx = transport.prepareRequest(ctx);
128
+
129
+ return transport.execute<T>(preparedCtx, responseType)
130
+ .catch((error: Error) => {
131
+ this.logger.warn('execute:transport-failed', transport.type, error);
132
+ return tryTransport(index + 1, error);
133
+ });
134
+ };
135
+
136
+ return tryTransport(0, null);
137
+ }
138
+
139
+ private validateParams(url: string, pathParams?: Record<string, any>, data?: Record<string, any>): void {
140
+ const matches = url.match(ApiService.PARAM_PATTERN) ?? [];
141
+ for (const match of matches) {
142
+ const paramName = match.slice(1);
143
+ const inPathParams = pathParams !== undefined && paramName in pathParams;
144
+ const inData = data !== undefined && paramName in data;
145
+ if (!inPathParams && !inData) {
146
+ throw new Error(`Missing path param '${match}'`);
147
+ }
148
+ }
149
+ }
150
+
151
+ private genCacheKey(request: IBuiltRequest, ctx: IHttpRequest): string {
152
+ const parts = [
153
+ request.method,
154
+ ctx.url,
155
+ JSON.stringify(ctx.data ?? {}),
156
+ request.pagination?.page ?? 0
157
+ ];
158
+ return 'api-cache:' + btoa(parts.join('|')).slice(0, 24);
159
+ }
160
+
161
+ private getFromCache<T>(request: IBuiltRequest, ctx: IHttpRequest): IHttpResponse<T> | null {
162
+ const key = this.genCacheKey(request, ctx);
163
+ const storage = request.cache?.storage ?? 'session';
164
+
165
+ let entry: ICacheEntry | null = null;
166
+
167
+ if (storage === 'session') {
168
+ const raw = sessionStorage.getItem(key);
169
+ entry = raw ? JSON.parse(raw) : null;
170
+ } else {
171
+ entry = this.memoryCache.get(key) ?? null;
172
+ }
173
+
174
+ if (!entry) {
175
+ return null;
176
+ }
177
+
178
+ // Check TTL
179
+ if (entry.expires && Date.now() > entry.expires) {
180
+ if (storage === 'session') {
181
+ sessionStorage.removeItem(key);
182
+ } else {
183
+ this.memoryCache.delete(key);
184
+ }
185
+ return null;
186
+ }
187
+
188
+ return entry.data;
189
+ }
190
+
191
+ private storeInCache(request: IBuiltRequest, ctx: IHttpRequest, response: IHttpResponse): void {
192
+ const key = this.genCacheKey(request, ctx);
193
+ const storage = request.cache?.storage ?? 'session';
194
+ const ttl = request.cache?.ttl;
195
+
196
+ const entry: ICacheEntry = {
197
+ data: response,
198
+ expires: ttl ? Date.now() + (ttl * 1000) : null,
199
+ created: Date.now()
200
+ };
201
+
202
+ if (storage === 'session') {
203
+ sessionStorage.setItem(key, JSON.stringify(entry));
204
+ } else {
205
+ this.memoryCache.set(key, entry);
206
+ }
207
+ }
208
+ }
209
+
210
+ export class ApiRequestBuilder {
211
+ private _url: string;
212
+ private _method: string = 'GET';
213
+ private _data: any;
214
+ private _pathParams: Record<string, any> | undefined;
215
+ private _queryString: Record<string, any> = {};
216
+ private _headers: Record<string, string> = {};
217
+ private _inputCodecs: ICodec[] = [];
218
+ private _outputCodecs: ICodec[] = [];
219
+ private _pagination: IPaginationConfig | null = null;
220
+ private _cache: ICacheConfig | null = null;
221
+ private _contentType: string = 'application/json';
222
+ private _accept: string = 'application/json';
223
+ private _responseType: 'json' | 'text' | 'blob' | 'arraybuffer' | 'auto' = 'auto';
224
+
225
+ constructor(
226
+ private api: ApiService,
227
+ path: string,
228
+ pathParams?: Record<string, any>
229
+ ) {
230
+ this._url = path;
231
+ this._pathParams = pathParams;
232
+ }
233
+
234
+ // Query string
235
+ public queryString(params: Record<string, any>): this {
236
+ this._queryString = {...this._queryString, ...params};
237
+ return this;
238
+ }
239
+
240
+ // Format entrée
241
+ public fromJson<T>(data?: T): this {
242
+ this._contentType = 'application/json';
243
+ if (data !== undefined) {
244
+ this._data = data;
245
+ }
246
+ return this;
247
+ }
248
+
249
+ public fromForm<T>(data?: T): this {
250
+ this._contentType = 'application/x-www-form-urlencoded';
251
+ if (data !== undefined) {
252
+ this._data = data;
253
+ }
254
+ return this;
255
+ }
256
+
257
+ public fromMultipart(data?: FormData): this {
258
+ this._contentType = 'multipart/form-data';
259
+ if (data !== undefined) {
260
+ this._data = data;
261
+ }
262
+ return this;
263
+ }
264
+
265
+ // Format sortie
266
+ public toJson(): this {
267
+ this._accept = 'application/json';
268
+ this._responseType = 'json';
269
+ return this;
270
+ }
271
+
272
+ public toXls(): this {
273
+ this._accept = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
274
+ this._responseType = 'blob';
275
+ return this;
276
+ }
277
+
278
+ public toBlob(): this {
279
+ this._accept = 'application/octet-stream';
280
+ this._responseType = 'blob';
281
+ return this;
282
+ }
283
+
284
+ public toText(): this {
285
+ this._accept = 'text/plain';
286
+ this._responseType = 'text';
287
+ return this;
288
+ }
289
+
290
+ public toArrayBuffer(): this {
291
+ this._accept = 'application/octet-stream';
292
+ this._responseType = 'arraybuffer';
293
+ return this;
294
+ }
295
+
296
+ // Codecs
297
+ public withInputCodec(codec: ICodec): this {
298
+ this._inputCodecs.push(codec);
299
+ return this;
300
+ }
301
+
302
+ public withOutputCodec(codec: ICodec): this {
303
+ this._outputCodecs.push(codec);
304
+ return this;
305
+ }
306
+
307
+ // Pagination & Cache
308
+ public withPagination(config?: IPaginationConfig): this {
309
+ this._pagination = {pageSize: 20, ...config};
310
+ return this;
311
+ }
312
+
313
+ public withCache(config?: ICacheConfig): this {
314
+ this._cache = {storage: 'session', ...config};
315
+ return this;
316
+ }
317
+
318
+ // Execution
319
+ public get<T>(): Promise<IHttpResponse<T>> {
320
+ this._method = 'GET';
321
+ return this.api.execute<T>(this.build());
322
+ }
323
+
324
+ public post<T>(): Promise<IHttpResponse<T>> {
325
+ this._method = 'POST';
326
+ return this.api.execute<T>(this.build());
327
+ }
328
+
329
+ public put<T>(): Promise<IHttpResponse<T>> {
330
+ this._method = 'PUT';
331
+ return this.api.execute<T>(this.build());
332
+ }
333
+
334
+ public patch<T>(): Promise<IHttpResponse<T>> {
335
+ this._method = 'PATCH';
336
+ return this.api.execute<T>(this.build());
337
+ }
338
+
339
+ public delete<T>(): Promise<IHttpResponse<T>> {
340
+ this._method = 'DELETE';
341
+ return this.api.execute<T>(this.build());
342
+ }
343
+
344
+ public request<T>(verb: string): Promise<IHttpResponse<T>> {
345
+ this._method = verb.toUpperCase();
346
+ return this.api.execute<T>(this.build());
347
+ }
348
+
349
+ private build(): IBuiltRequest {
350
+ return {
351
+ url: this.appendQueryString(this._url),
352
+ method: this._method,
353
+ headers: {
354
+ 'Content-Type': this._contentType,
355
+ 'Accept': this._accept,
356
+ ...this._headers
357
+ },
358
+ data: this._data,
359
+ pathParams: this._pathParams,
360
+ responseType: this._responseType,
361
+ inputCodecs: this._inputCodecs,
362
+ outputCodecs: this._outputCodecs,
363
+ pagination: this._pagination,
364
+ cache: this._cache
365
+ };
366
+ }
367
+
368
+ private appendQueryString(url: string): string {
369
+ if (Object.keys(this._queryString).length === 0) {
370
+ return url;
371
+ }
372
+
373
+ const params = new URLSearchParams();
374
+ for (const [key, value] of Object.entries(this._queryString)) {
375
+ if (value !== undefined && value !== null) {
376
+ params.append(key, String(value));
377
+ }
378
+ }
379
+
380
+ const qs = params.toString();
381
+ if (!qs) {
382
+ return url;
383
+ }
384
+
385
+ return url.includes('?') ? `${url}&${qs}` : `${url}?${qs}`;
386
+ }
387
+ }
@@ -0,0 +1,166 @@
1
+ import {DI, ILogger, resolve} from 'aurelia';
2
+ import {HttpClientConfiguration, IHttpClient} from '@aurelia/fetch-client';
3
+ import {IHttpRequest, IHttpResponse, ITransport} from '../interfaces/api';
4
+ import {Transport} from '../enums/api';
5
+ import {IBleetConfiguration} from '../configure';
6
+
7
+ export interface IHttpService extends HttpService {}
8
+ export const IHttpService = DI.createInterface<IHttpService>(
9
+ 'IHttpService',
10
+ (x) => x.singleton(HttpService)
11
+ );
12
+
13
+ export class HttpService implements ITransport {
14
+ public readonly type = Transport.Http;
15
+ public constructor(
16
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('HttpService'),
17
+ private readonly httpClient: IHttpClient = resolve(IHttpClient),
18
+ private readonly config: IBleetConfiguration = resolve(IBleetConfiguration),
19
+ ) {
20
+ this.logger.trace('constructor');
21
+ this.httpClient.configure((config: HttpClientConfiguration) => {
22
+ config.withDefaults({
23
+ headers: {
24
+ 'X-Requested-With': 'XMLHttpRequest',
25
+ },
26
+ credentials: 'include'
27
+ });
28
+ return config;
29
+ });
30
+ }
31
+
32
+ public isAvailable(): boolean {
33
+ return true;
34
+ }
35
+
36
+ public prepareRequest(ctx: IHttpRequest): IHttpRequest {
37
+ const baseUrl = this.config.getBaseUrl(Transport.Http);
38
+ let url = baseUrl + ctx.url;
39
+ const pathParams = ctx.pathParams ?? {};
40
+
41
+ // FormData: don't try to extract path params from it, keep as-is
42
+ if (ctx.data instanceof FormData) {
43
+ // Substitute :params in URL from pathParams only
44
+ const paramPattern = /:[a-zA-Z_][a-zA-Z0-9_-]*/g;
45
+ const matches = ctx.url.match(paramPattern) ?? [];
46
+
47
+ for (const match of matches) {
48
+ const paramName = match.slice(1);
49
+ if (paramName in pathParams) {
50
+ url = url.replace(match, encodeURIComponent(String(pathParams[paramName])));
51
+ }
52
+ }
53
+
54
+ return {
55
+ url,
56
+ method: ctx.method,
57
+ headers: ctx.headers,
58
+ data: ctx.data
59
+ };
60
+ }
61
+
62
+ const remainingData = {...ctx.data};
63
+
64
+ // Substitute :params in URL (explicit > implicit) and remove from payload
65
+ const paramPattern = /:[a-zA-Z_][a-zA-Z0-9_-]*/g;
66
+ const matches = ctx.url.match(paramPattern) ?? [];
67
+
68
+ for (const match of matches) {
69
+ const paramName = match.slice(1);
70
+ const value = paramName in pathParams ? pathParams[paramName] : remainingData[paramName];
71
+ url = url.replace(match, encodeURIComponent(String(value)));
72
+ delete remainingData[paramName];
73
+ }
74
+
75
+ return {
76
+ url,
77
+ method: ctx.method,
78
+ headers: ctx.headers,
79
+ data: remainingData
80
+ };
81
+ }
82
+
83
+ public execute<T>(
84
+ ctx: IHttpRequest,
85
+ responseType: 'json' | 'text' | 'blob' | 'arraybuffer' | 'auto' = 'json'
86
+ ): Promise<IHttpResponse<T>> {
87
+ this.logger.trace('execute', ctx.method, ctx.url);
88
+
89
+ const hasBody = ['POST', 'PATCH', 'PUT', 'DELETE'].includes(ctx.method.toUpperCase());
90
+ const headers = { ...ctx.headers };
91
+
92
+ const init: RequestInit = {
93
+ method: ctx.method.toUpperCase(),
94
+ };
95
+
96
+ if (hasBody && ctx.data) {
97
+ if (ctx.data instanceof FormData) {
98
+ // FormData: don't set Content-Type, browser will set it with boundary
99
+ delete headers['Content-Type'];
100
+ init.body = ctx.data;
101
+ } else if (Object.keys(ctx.data).length > 0) {
102
+ init.body = JSON.stringify(ctx.data);
103
+ }
104
+ }
105
+
106
+ init.headers = headers;
107
+
108
+ return this.httpClient.fetch(ctx.url, init)
109
+ .then((response) => this.parseResponse<T>(response, responseType));
110
+ }
111
+
112
+ private parseResponse<T>(response: Response, responseType: string): Promise<IHttpResponse<T>> {
113
+ const headers: Record<string, string> = {};
114
+ response.headers.forEach((value, key) => {
115
+ headers[key] = value;
116
+ });
117
+
118
+ const effectiveType = responseType === 'auto'
119
+ ? this.detectResponseType(response.headers.get('Content-Type'))
120
+ : responseType;
121
+
122
+ return this.parseBody<T>(response, effectiveType)
123
+ .then((body) => ({
124
+ statusCode: response.status,
125
+ headers,
126
+ body
127
+ }));
128
+ }
129
+
130
+ private detectResponseType(contentType: string | null): 'json' | 'text' | 'blob' {
131
+ if (!contentType) {
132
+ return 'text';
133
+ }
134
+
135
+ const ct = contentType.toLowerCase();
136
+
137
+ if (ct.includes('application/json')) {
138
+ return 'json';
139
+ }
140
+
141
+ if (ct.startsWith('text/')) {
142
+ return 'text';
143
+ }
144
+
145
+ if (ct.includes('application/')) {
146
+ return 'blob';
147
+ }
148
+
149
+ return 'text';
150
+ }
151
+
152
+ private parseBody<T>(response: Response, responseType: string): Promise<T> {
153
+ switch (responseType) {
154
+ case 'json':
155
+ return response.json();
156
+ case 'text':
157
+ return response.text() as Promise<T>;
158
+ case 'blob':
159
+ return response.blob() as Promise<T>;
160
+ case 'arraybuffer':
161
+ return response.arrayBuffer() as Promise<T>;
162
+ default:
163
+ return response.json();
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,138 @@
1
+ import {DI, ILogger, resolve} from 'aurelia';
2
+ import type {Socket} from 'socket.io-client';
3
+ import {IHttpRequest, IHttpResponse, ITransport} from '../interfaces/api';
4
+ import {Transport} from '../enums/api';
5
+ import {IBleetConfiguration} from '../configure';
6
+
7
+ // io sera chargé à la demande via require()
8
+ let io: typeof import('socket.io-client').io | null = null;
9
+
10
+ function getSocketIo(): typeof import('socket.io-client').io {
11
+ if (!io) {
12
+ try {
13
+ io = require('socket.io-client').io;
14
+ } catch {
15
+ throw new Error(
16
+ 'socket.io-client non installé. ' +
17
+ 'Installez-le avec : npm install socket.io-client'
18
+ );
19
+ }
20
+ }
21
+ return io;
22
+ }
23
+
24
+ export interface ISocketioService extends SocketioService {}
25
+ export const ISocketioService = DI.createInterface<ISocketioService>(
26
+ 'ISocketioService',
27
+ (x) => x.singleton(SocketioService)
28
+ );
29
+
30
+ export class SocketioService implements ITransport {
31
+ public readonly type = Transport.Socketio;
32
+ private readonly timeout = 5000;
33
+ private socket: Socket | null = null;
34
+ private connected: boolean = false;
35
+
36
+ public constructor(
37
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('SocketioService'),
38
+ private readonly config: IBleetConfiguration = resolve(IBleetConfiguration),
39
+ ) {
40
+ this.logger.trace('constructor');
41
+ }
42
+
43
+ public isConnected(): boolean {
44
+ return this.connected && this.socket !== null;
45
+ }
46
+
47
+ public isAvailable(): boolean {
48
+ return this.isConnected();
49
+ }
50
+
51
+ public prepareRequest(ctx: IHttpRequest): IHttpRequest {
52
+ return {
53
+ ...ctx,
54
+ data: {...ctx.data, ...ctx.pathParams}
55
+ };
56
+ }
57
+
58
+ public connect(namespace: string = '/', options?: Record<string, any>): Promise<void> {
59
+ const baseUrl = this.config.getBaseUrl(Transport.Socketio);
60
+ const url = baseUrl + namespace;
61
+ this.logger.trace('connect', url);
62
+
63
+ if (this.socket !== null) {
64
+ this.logger.warn('connect:already-connected');
65
+ return Promise.resolve();
66
+ }
67
+
68
+ return new Promise((resolve, reject) => {
69
+ const socketIo = getSocketIo();
70
+ this.socket = socketIo(url, {
71
+ transports: ['websocket'],
72
+ ...options
73
+ });
74
+
75
+ this.socket.on('connect', () => {
76
+ this.logger.trace('connect:success');
77
+ this.connected = true;
78
+ resolve();
79
+ });
80
+
81
+ this.socket.on('connect_error', (error: Error) => {
82
+ this.logger.error('connect:error', error);
83
+ this.connected = false;
84
+ reject(error);
85
+ });
86
+
87
+ this.socket.on('disconnect', (reason: string) => {
88
+ this.logger.trace('disconnect', reason);
89
+ this.connected = false;
90
+ });
91
+ });
92
+ }
93
+
94
+ public disconnect(): void {
95
+ this.logger.trace('disconnect');
96
+
97
+ if (this.socket !== null) {
98
+ this.socket.disconnect();
99
+ this.socket = null;
100
+ this.connected = false;
101
+ }
102
+ }
103
+
104
+ public execute<T>(ctx: IHttpRequest, responseType?: string): Promise<IHttpResponse<T>> {
105
+ this.logger.trace('execute', ctx.method, ctx.url);
106
+
107
+ if (!this.isConnected() || this.socket === null) {
108
+ return Promise.reject(new Error('Socket not connected'));
109
+ }
110
+
111
+ const channel = `${ctx.method.toLowerCase()}:${ctx.url}`;
112
+ let data = ctx.data ?? {};
113
+
114
+ // Convert FormData to plain object (Socket.io can't send FormData)
115
+ if (data instanceof FormData) {
116
+ const obj: Record<string, any> = {};
117
+ data.forEach((value, key) => {
118
+ // Skip File objects - can't be serialized for Socket.io
119
+ if (!(value instanceof File)) {
120
+ obj[key] = value;
121
+ }
122
+ });
123
+ data = obj;
124
+ }
125
+
126
+ return new Promise((resolve, reject) => {
127
+ const timeoutId = setTimeout(() => {
128
+ reject(new Error('Socket timeout'));
129
+ }, this.timeout);
130
+
131
+ this.socket!.emit(channel, data, (response: IHttpResponse<T>) => {
132
+ clearTimeout(timeoutId);
133
+ this.logger.trace('execute:response', channel, response);
134
+ resolve(response);
135
+ });
136
+ });
137
+ }
138
+ }