@mysteryinfosolutions/api-core 1.8.0 → 1.9.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.
- package/README.md +930 -33
- package/fesm2022/mysteryinfosolutions-api-core.mjs +810 -47
- package/fesm2022/mysteryinfosolutions-api-core.mjs.map +1 -1
- package/index.d.ts +843 -24
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Component } from '@angular/core';
|
|
3
|
-
import { BehaviorSubject, map } from 'rxjs';
|
|
2
|
+
import { InjectionToken, Component, Injectable, inject } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, distinctUntilChanged, shareReplay, map, catchError, throwError } from 'rxjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* BaseResourceConfig<T>
|
|
@@ -66,6 +66,92 @@ class BaseResourceConfig {
|
|
|
66
66
|
permissions;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Default configuration values for the api-core library.
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_API_CORE_CONFIG = {
|
|
73
|
+
pagination: {
|
|
74
|
+
defaultPage: 1,
|
|
75
|
+
defaultPageLength: 10,
|
|
76
|
+
pageLengthOptions: [10, 25, 50, 100],
|
|
77
|
+
maxPageLength: 1000
|
|
78
|
+
},
|
|
79
|
+
api: {
|
|
80
|
+
dateFormat: 'iso',
|
|
81
|
+
dateFormatter: (date) => date.toISOString(),
|
|
82
|
+
includeCredentials: false,
|
|
83
|
+
defaultHeaders: {},
|
|
84
|
+
timeout: 30000 // 30 seconds
|
|
85
|
+
},
|
|
86
|
+
queryString: {
|
|
87
|
+
arrayFormat: 'brackets',
|
|
88
|
+
arrayFormatter: (key, values) => `${key}=[${values.join(',')}]`,
|
|
89
|
+
encode: true,
|
|
90
|
+
encoder: (value) => encodeURIComponent(value)
|
|
91
|
+
},
|
|
92
|
+
sort: {
|
|
93
|
+
defaultOrder: 'ASC',
|
|
94
|
+
separator: ':',
|
|
95
|
+
allowMultiSort: true
|
|
96
|
+
},
|
|
97
|
+
errorHandling: {
|
|
98
|
+
logErrors: true,
|
|
99
|
+
showUserMessages: true,
|
|
100
|
+
onError: (error) => console.error('API Error:', error)
|
|
101
|
+
},
|
|
102
|
+
loading: {
|
|
103
|
+
minLoadingTime: 0,
|
|
104
|
+
showLoadingByDefault: true
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Injection token for api-core configuration.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Provide custom configuration
|
|
112
|
+
* providers: [
|
|
113
|
+
* {
|
|
114
|
+
* provide: API_CORE_CONFIG,
|
|
115
|
+
* useValue: {
|
|
116
|
+
* pagination: { defaultPageLength: 25 }
|
|
117
|
+
* } as ApiCoreConfig
|
|
118
|
+
* }
|
|
119
|
+
* ]
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // Inject in service
|
|
123
|
+
* constructor(@Inject(API_CORE_CONFIG) private config: ApiCoreConfig) {}
|
|
124
|
+
*/
|
|
125
|
+
const API_CORE_CONFIG = new InjectionToken('api-core.config', {
|
|
126
|
+
providedIn: 'root',
|
|
127
|
+
factory: () => DEFAULT_API_CORE_CONFIG
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Merges user-provided configuration with default values.
|
|
131
|
+
*
|
|
132
|
+
* @param userConfig User-provided configuration
|
|
133
|
+
* @returns Merged configuration with defaults
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const config = mergeConfig({
|
|
137
|
+
* pagination: { defaultPageLength: 25 }
|
|
138
|
+
* });
|
|
139
|
+
* // Returns full config with defaultPageLength: 25 and all other defaults
|
|
140
|
+
*/
|
|
141
|
+
function mergeConfig(userConfig) {
|
|
142
|
+
if (!userConfig) {
|
|
143
|
+
return DEFAULT_API_CORE_CONFIG;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
pagination: { ...DEFAULT_API_CORE_CONFIG.pagination, ...userConfig.pagination },
|
|
147
|
+
api: { ...DEFAULT_API_CORE_CONFIG.api, ...userConfig.api },
|
|
148
|
+
queryString: { ...DEFAULT_API_CORE_CONFIG.queryString, ...userConfig.queryString },
|
|
149
|
+
sort: { ...DEFAULT_API_CORE_CONFIG.sort, ...userConfig.sort },
|
|
150
|
+
errorHandling: { ...DEFAULT_API_CORE_CONFIG.errorHandling, ...userConfig.errorHandling },
|
|
151
|
+
loading: { ...DEFAULT_API_CORE_CONFIG.loading, ...userConfig.loading }
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
69
155
|
class ApiCore {
|
|
70
156
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiCore, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
71
157
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.2", type: ApiCore, isStandalone: true, selector: "lib-api-core", ngImport: i0, template: `
|
|
@@ -83,12 +169,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImpor
|
|
|
83
169
|
` }]
|
|
84
170
|
}] });
|
|
85
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Enum defining column selection modes for API queries.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* Used to control which columns are returned in API responses,
|
|
177
|
+
* useful for optimizing payload size and performance.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* const filter = {
|
|
181
|
+
* selectMode: SELECT_MODE.ONLYCOLUMNS,
|
|
182
|
+
* selectColumns: 'id,name,email'
|
|
183
|
+
* };
|
|
184
|
+
*/
|
|
86
185
|
var SELECT_MODE;
|
|
87
186
|
(function (SELECT_MODE) {
|
|
187
|
+
/** Return all columns (default behavior) */
|
|
88
188
|
SELECT_MODE[SELECT_MODE["ALL"] = 1] = "ALL";
|
|
189
|
+
/** Return only specified columns via selectColumns parameter */
|
|
89
190
|
SELECT_MODE[SELECT_MODE["ONLYCOLUMNS"] = 2] = "ONLYCOLUMNS";
|
|
90
191
|
})(SELECT_MODE || (SELECT_MODE = {}));
|
|
91
|
-
;
|
|
92
192
|
|
|
93
193
|
/**
|
|
94
194
|
* Generates a permission mapping object for a given resource.
|
|
@@ -164,50 +264,183 @@ function generatePermissions(resource, extra = []) {
|
|
|
164
264
|
}, {});
|
|
165
265
|
}
|
|
166
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Converts an array of SortItem objects into a comma-separated sort string.
|
|
269
|
+
*
|
|
270
|
+
* @param sortItems Array of sort configurations
|
|
271
|
+
* @returns Formatted sort string (e.g., "name:ASC,createdAt:DESC") or empty string
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* const sorts = [
|
|
275
|
+
* new SortItem('name', 'ASC'),
|
|
276
|
+
* new SortItem('createdAt', 'DESC')
|
|
277
|
+
* ];
|
|
278
|
+
* const sortString = sortObjectToString(sorts);
|
|
279
|
+
* // Returns: "name:ASC,createdAt:DESC"
|
|
280
|
+
*/
|
|
167
281
|
const sortObjectToString = (sortItems) => {
|
|
168
282
|
if (!Array.isArray(sortItems) || sortItems.length === 0)
|
|
169
283
|
return '';
|
|
170
284
|
return sortItems.map(s => `${s.field}:${s.order}`).join(',');
|
|
171
285
|
};
|
|
172
|
-
|
|
173
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Converts a filter object into a URL query string.
|
|
288
|
+
*
|
|
289
|
+
* @param filter Filter object with query parameters
|
|
290
|
+
* @returns URL query string with '?' prefix, or empty string if no parameters
|
|
291
|
+
*
|
|
292
|
+
* @remarks
|
|
293
|
+
* - Automatically converts SortItem arrays to sort strings
|
|
294
|
+
* - Encodes all values for URL safety
|
|
295
|
+
* - Filters out undefined and null values
|
|
296
|
+
* - Arrays are encoded as comma-separated values in brackets
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* const filter = {
|
|
300
|
+
* page: 1,
|
|
301
|
+
* pageLength: 25,
|
|
302
|
+
* search: 'John Doe',
|
|
303
|
+
* roles: ['admin', 'user'],
|
|
304
|
+
* sort: [new SortItem('name', 'ASC')]
|
|
305
|
+
* };
|
|
306
|
+
* const queryString = jsonToQueryString(filter);
|
|
307
|
+
* // Returns: "?page=1&pageLength=25&search=John%20Doe&roles=[admin,user]&sort=name:ASC"
|
|
308
|
+
*/
|
|
309
|
+
const jsonToQueryString = (filter) => {
|
|
310
|
+
if (!filter || typeof filter !== 'object')
|
|
174
311
|
return '';
|
|
175
|
-
|
|
176
|
-
|
|
312
|
+
// Clone to avoid mutating original
|
|
313
|
+
const params = { ...filter };
|
|
314
|
+
// Convert sort array to string format
|
|
315
|
+
if ('sort' in params && Array.isArray(params['sort'])) {
|
|
316
|
+
params['sort'] = sortObjectToString(params['sort']);
|
|
177
317
|
}
|
|
178
|
-
const queryArray = Object.keys(
|
|
179
|
-
.filter(key =>
|
|
318
|
+
const queryArray = Object.keys(params)
|
|
319
|
+
.filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
|
|
180
320
|
.map(key => {
|
|
181
|
-
|
|
182
|
-
|
|
321
|
+
const value = params[key];
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
// Encode arrays as [item1,item2,item3]
|
|
324
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent('[' + value.toString() + ']')}`;
|
|
183
325
|
}
|
|
184
326
|
else {
|
|
185
|
-
return `${encodeURIComponent(key)}=${encodeURIComponent(
|
|
327
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
|
|
186
328
|
}
|
|
187
329
|
});
|
|
188
330
|
return queryArray.length > 0 ? `?${queryArray.join('&')}` : '';
|
|
189
331
|
};
|
|
332
|
+
/**
|
|
333
|
+
* Checks if an object is empty (has no own properties).
|
|
334
|
+
*
|
|
335
|
+
* @param obj Object to check (any object type, null, or undefined)
|
|
336
|
+
* @returns True if object is null, undefined, or has no properties
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* isEmpty({}); // true
|
|
340
|
+
* isEmpty(null); // true
|
|
341
|
+
* isEmpty(undefined); // true
|
|
342
|
+
* isEmpty({ page: 1 }); // false
|
|
343
|
+
* isEmpty([]); // true (arrays with no length)
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* // Works with any object type
|
|
347
|
+
* interface MyType { id: number; name: string; }
|
|
348
|
+
* const myObj: MyType = { id: 1, name: 'test' };
|
|
349
|
+
* isEmpty(myObj); // false
|
|
350
|
+
*/
|
|
190
351
|
const isEmpty = (obj) => {
|
|
191
|
-
|
|
352
|
+
if (obj === null || obj === undefined) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
// Check if it's an array
|
|
356
|
+
if (Array.isArray(obj)) {
|
|
357
|
+
return obj.length === 0;
|
|
358
|
+
}
|
|
359
|
+
// Check if it's an object with no own properties
|
|
360
|
+
return Object.keys(obj).length === 0;
|
|
192
361
|
};
|
|
193
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Base filter class providing common filtering, pagination, and sorting capabilities.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* Extend this class in your custom filter types to add domain-specific filter properties.
|
|
368
|
+
* The base filter handles pagination, sorting, searching, and date range filtering.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* interface UserFilter extends Filter {
|
|
372
|
+
* role?: string;
|
|
373
|
+
* isActive?: boolean;
|
|
374
|
+
* departmentId?: number;
|
|
375
|
+
* }
|
|
376
|
+
*
|
|
377
|
+
* const filter: UserFilter = {
|
|
378
|
+
* page: 1,
|
|
379
|
+
* pageLength: 25,
|
|
380
|
+
* search: 'john',
|
|
381
|
+
* role: 'admin',
|
|
382
|
+
* sort: [new SortItem('name', 'ASC')]
|
|
383
|
+
* };
|
|
384
|
+
*/
|
|
194
385
|
class Filter {
|
|
195
|
-
|
|
386
|
+
/** Array of specific IDs to filter by */
|
|
387
|
+
ids;
|
|
388
|
+
/** Column name to use for date range filtering */
|
|
196
389
|
dateRangeColumn;
|
|
390
|
+
/** Start date for date range filter (ISO format) */
|
|
197
391
|
dateRangeFrom;
|
|
392
|
+
/** End date for date range filter (ISO format) */
|
|
198
393
|
dateRangeTo;
|
|
199
|
-
page
|
|
200
|
-
|
|
201
|
-
|
|
394
|
+
/** Current page number (1-based) */
|
|
395
|
+
page;
|
|
396
|
+
/** Number of records per page */
|
|
397
|
+
pageLength;
|
|
398
|
+
/** Array of sort configurations */
|
|
399
|
+
sort;
|
|
400
|
+
/** Search query string */
|
|
202
401
|
search;
|
|
402
|
+
/** Comma-separated list of columns to search in */
|
|
203
403
|
searchColumns;
|
|
404
|
+
/** Comma-separated list of columns to select/return */
|
|
204
405
|
selectColumns;
|
|
406
|
+
/** Mode for column selection */
|
|
205
407
|
selectMode;
|
|
206
408
|
}
|
|
207
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Represents a single sort configuration for a field.
|
|
412
|
+
*
|
|
413
|
+
* @remarks
|
|
414
|
+
* Used in filter objects to specify sorting criteria.
|
|
415
|
+
* Multiple SortItem instances can be combined for multi-column sorting.
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* // Single sort
|
|
419
|
+
* const sort = new SortItem('name', 'ASC');
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* // Multi-column sort
|
|
423
|
+
* const sorts = [
|
|
424
|
+
* new SortItem('priority', 'DESC'),
|
|
425
|
+
* new SortItem('createdAt', 'DESC'),
|
|
426
|
+
* new SortItem('name', 'ASC')
|
|
427
|
+
* ];
|
|
428
|
+
*
|
|
429
|
+
* const filter = {
|
|
430
|
+
* page: 1,
|
|
431
|
+
* pageLength: 25,
|
|
432
|
+
* sort: sorts
|
|
433
|
+
* };
|
|
434
|
+
*/
|
|
208
435
|
class SortItem {
|
|
209
436
|
field;
|
|
210
437
|
order;
|
|
438
|
+
/**
|
|
439
|
+
* Creates a sort configuration.
|
|
440
|
+
*
|
|
441
|
+
* @param field The field/column name to sort by
|
|
442
|
+
* @param order Sort direction: 'ASC' (ascending) or 'DESC' (descending)
|
|
443
|
+
*/
|
|
211
444
|
constructor(field, order) {
|
|
212
445
|
this.field = field;
|
|
213
446
|
this.order = order;
|
|
@@ -217,7 +450,78 @@ class SortItem {
|
|
|
217
450
|
;
|
|
218
451
|
|
|
219
452
|
/**
|
|
220
|
-
*
|
|
453
|
+
* Generic reactive state management service for data-driven features.
|
|
454
|
+
*
|
|
455
|
+
* @template TRecord The type of record/entity being managed
|
|
456
|
+
* @template TFilter Filter type extending Partial<TRecord> & Filter
|
|
457
|
+
*
|
|
458
|
+
* @remarks
|
|
459
|
+
* BaseStateService provides comprehensive state management for list-based views including:
|
|
460
|
+
* - Filter state with pagination and sorting
|
|
461
|
+
* - Records collection with CRUD helpers
|
|
462
|
+
* - Loading states (with context keys)
|
|
463
|
+
* - Error handling
|
|
464
|
+
* - Record selection
|
|
465
|
+
* - Pagination metadata
|
|
466
|
+
*
|
|
467
|
+
* All state is reactive using RxJS BehaviorSubjects, making it easy to integrate
|
|
468
|
+
* with Angular templates using the async pipe.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* // Basic setup in component
|
|
472
|
+
* @Component({
|
|
473
|
+
* selector: 'app-users',
|
|
474
|
+
* providers: [BaseStateService] // Component-level instance
|
|
475
|
+
* })
|
|
476
|
+
* export class UsersComponent implements OnInit, OnDestroy {
|
|
477
|
+
* state = new BaseStateService<User, UserFilter>();
|
|
478
|
+
*
|
|
479
|
+
* constructor(private userService: UserService) {}
|
|
480
|
+
*
|
|
481
|
+
* ngOnInit() {
|
|
482
|
+
* // Subscribe to filter changes
|
|
483
|
+
* this.state.filter$.subscribe(filter => {
|
|
484
|
+
* this.loadUsers(filter);
|
|
485
|
+
* });
|
|
486
|
+
*
|
|
487
|
+
* // Set initial filter
|
|
488
|
+
* this.state.setFilter({ page: 1, pageLength: 25 });
|
|
489
|
+
* }
|
|
490
|
+
*
|
|
491
|
+
* loadUsers(filter: UserFilter) {
|
|
492
|
+
* this.state.setLoading('list', true);
|
|
493
|
+
*
|
|
494
|
+
* this.userService.getAll(filter).subscribe({
|
|
495
|
+
* next: (response) => {
|
|
496
|
+
* if (response.data) {
|
|
497
|
+
* this.state.setApiResponse(response.data);
|
|
498
|
+
* }
|
|
499
|
+
* this.state.setLoading('list', false);
|
|
500
|
+
* },
|
|
501
|
+
* error: (err) => {
|
|
502
|
+
* this.state.setError('Failed to load users');
|
|
503
|
+
* this.state.setLoading('list', false);
|
|
504
|
+
* }
|
|
505
|
+
* });
|
|
506
|
+
* }
|
|
507
|
+
*
|
|
508
|
+
* ngOnDestroy() {
|
|
509
|
+
* this.state.destroy();
|
|
510
|
+
* }
|
|
511
|
+
* }
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* // Using in template
|
|
515
|
+
* <div *ngIf="state.isLoading$('list') | async">Loading...</div>
|
|
516
|
+
* <div *ngIf="state.error$ | async as error">{{ error }}</div>
|
|
517
|
+
*
|
|
518
|
+
* <div *ngFor="let user of state.records$ | async">
|
|
519
|
+
* {{ user.name }}
|
|
520
|
+
* </div>
|
|
521
|
+
*
|
|
522
|
+
* <div *ngIf="state.pager$ | async as pager">
|
|
523
|
+
* Page {{ pager.currentPage }} of {{ pager.lastPage }}
|
|
524
|
+
* </div>
|
|
221
525
|
*/
|
|
222
526
|
class BaseStateService {
|
|
223
527
|
filterSubject = new BehaviorSubject({ page: 1, pageLength: 10 });
|
|
@@ -226,18 +530,36 @@ class BaseStateService {
|
|
|
226
530
|
selectedSubject = new BehaviorSubject(null);
|
|
227
531
|
loadingMapSubject = new BehaviorSubject({});
|
|
228
532
|
errorSubject = new BehaviorSubject(null);
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Observable stream of current filter.
|
|
535
|
+
* Optimized with distinctUntilChanged to prevent duplicate emissions.
|
|
536
|
+
*/
|
|
537
|
+
filter$ = this.filterSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
538
|
+
/**
|
|
539
|
+
* Observable stream of current records.
|
|
540
|
+
* Optimized with distinctUntilChanged for reference equality.
|
|
541
|
+
*/
|
|
542
|
+
records$ = this.recordsSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
543
|
+
/**
|
|
544
|
+
* Observable stream of current pager metadata.
|
|
545
|
+
* Optimized with distinctUntilChanged for deep equality.
|
|
546
|
+
*/
|
|
547
|
+
pager$ = this.pagerSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
548
|
+
/**
|
|
549
|
+
* Observable stream of the currently selected record.
|
|
550
|
+
* Optimized with distinctUntilChanged for reference equality.
|
|
551
|
+
*/
|
|
552
|
+
selected$ = this.selectedSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
553
|
+
/**
|
|
554
|
+
* Observable stream of loading state.
|
|
555
|
+
* Optimized with distinctUntilChanged for deep equality.
|
|
556
|
+
*/
|
|
557
|
+
loading$ = this.loadingMapSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
558
|
+
/**
|
|
559
|
+
* Observable stream of current error message.
|
|
560
|
+
* Optimized with distinctUntilChanged for value equality.
|
|
561
|
+
*/
|
|
562
|
+
error$ = this.errorSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
241
563
|
/** Returns the current filter. */
|
|
242
564
|
get currentFilter() {
|
|
243
565
|
return this.filterSubject.value;
|
|
@@ -288,24 +610,45 @@ class BaseStateService {
|
|
|
288
610
|
this.setPager(response.pager);
|
|
289
611
|
}
|
|
290
612
|
/**
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
613
|
+
* Updates the sort order in the current filter.
|
|
614
|
+
* Toggles order if column already exists and no explicit sort is provided, or adds it otherwise.
|
|
615
|
+
*
|
|
616
|
+
* @param column Column name to sort by
|
|
617
|
+
* @param sort Optional sort order. If not provided and column exists, toggles between ASC/DESC.
|
|
618
|
+
* If not provided and column doesn't exist, defaults to ASC.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* // Add new sort (defaults to ASC)
|
|
622
|
+
* state.setSort('name');
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* // Toggle existing sort
|
|
626
|
+
* state.setSort('name'); // If already ASC, becomes DESC; if DESC, becomes ASC
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* // Explicitly set sort order
|
|
630
|
+
* state.setSort('name', 'DESC'); // Always sets to DESC
|
|
631
|
+
*/
|
|
632
|
+
setSort(column, sort) {
|
|
296
633
|
const current = this.filterSubject.value;
|
|
297
634
|
let sortItems = [...(current.sort ?? [])];
|
|
298
635
|
const index = sortItems.findIndex(item => item.field === column);
|
|
299
636
|
if (index !== -1) {
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
637
|
+
// Column already exists in sort
|
|
638
|
+
if (sort !== undefined) {
|
|
639
|
+
// Explicit sort provided - use it
|
|
640
|
+
sortItems[index] = new SortItem(sortItems[index].field, sort);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
// No explicit sort - toggle the order
|
|
644
|
+
const currentOrder = sortItems[index].order;
|
|
645
|
+
const newOrder = currentOrder === 'ASC' ? 'DESC' : 'ASC';
|
|
646
|
+
sortItems[index] = new SortItem(sortItems[index].field, newOrder);
|
|
647
|
+
}
|
|
306
648
|
}
|
|
307
649
|
else {
|
|
308
|
-
|
|
650
|
+
// Column doesn't exist - add it (default to ASC if not specified)
|
|
651
|
+
sortItems.push(new SortItem(column, sort ?? 'ASC'));
|
|
309
652
|
}
|
|
310
653
|
this.filterSubject.next({
|
|
311
654
|
...current,
|
|
@@ -340,9 +683,13 @@ class BaseStateService {
|
|
|
340
683
|
*
|
|
341
684
|
* @param key A unique key representing the loading context (e.g., "list", "detail")
|
|
342
685
|
* @returns Observable emitting `true` if the given context is loading, `false` otherwise.
|
|
686
|
+
*
|
|
687
|
+
* @remarks
|
|
688
|
+
* Optimized with distinctUntilChanged to prevent duplicate emissions
|
|
689
|
+
* and shareReplay to share subscriptions.
|
|
343
690
|
*/
|
|
344
691
|
isLoading$(key) {
|
|
345
|
-
return this.loading$.pipe(map(state => !!state[key]));
|
|
692
|
+
return this.loading$.pipe(map(state => !!state[key]), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
346
693
|
}
|
|
347
694
|
/**
|
|
348
695
|
* Sets the loading state for a specific key.
|
|
@@ -476,36 +823,452 @@ class BaseStateService {
|
|
|
476
823
|
}
|
|
477
824
|
}
|
|
478
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Abstract base service providing standard CRUD operations for REST APIs.
|
|
828
|
+
*
|
|
829
|
+
* @template T The full model type representing your entity
|
|
830
|
+
* @template TFilter Filter type extending Partial<T> & Filter for query parameters
|
|
831
|
+
* @template TCreate DTO type for creating new records (defaults to Partial<T>)
|
|
832
|
+
* @template TUpdate DTO type for updating records (defaults to Partial<T>)
|
|
833
|
+
*
|
|
834
|
+
* @remarks
|
|
835
|
+
* Extend this class in your feature services to get type-safe CRUD operations
|
|
836
|
+
* with minimal boilerplate. The service automatically handles:
|
|
837
|
+
* - Query string generation from filters
|
|
838
|
+
* - Pagination and sorting
|
|
839
|
+
* - Standardized response/error handling
|
|
840
|
+
* - Type safety throughout the request/response cycle
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* // Define your model
|
|
844
|
+
* interface User {
|
|
845
|
+
* id: number;
|
|
846
|
+
* name: string;
|
|
847
|
+
* email: string;
|
|
848
|
+
* role: string;
|
|
849
|
+
* }
|
|
850
|
+
*
|
|
851
|
+
* // Define custom filter
|
|
852
|
+
* interface UserFilter extends Filter {
|
|
853
|
+
* role?: string;
|
|
854
|
+
* isActive?: boolean;
|
|
855
|
+
* }
|
|
856
|
+
*
|
|
857
|
+
* // Create service
|
|
858
|
+
* @Injectable({ providedIn: 'root' })
|
|
859
|
+
* export class UserService extends BaseService<User, UserFilter> {
|
|
860
|
+
* constructor(http: HttpClient) {
|
|
861
|
+
* super(http, '/api/users');
|
|
862
|
+
* }
|
|
863
|
+
*
|
|
864
|
+
* // Add custom methods
|
|
865
|
+
* activateUser(id: number): Observable<IResponse<User>> {
|
|
866
|
+
* return this.http.post<IResponse<User>>(`${this.baseUrl}/${id}/activate`, {});
|
|
867
|
+
* }
|
|
868
|
+
* }
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* // With separate DTOs for create/update
|
|
872
|
+
* interface CreateUserDto {
|
|
873
|
+
* name: string;
|
|
874
|
+
* email: string;
|
|
875
|
+
* password: string;
|
|
876
|
+
* }
|
|
877
|
+
*
|
|
878
|
+
* interface UpdateUserDto {
|
|
879
|
+
* name?: string;
|
|
880
|
+
* email?: string;
|
|
881
|
+
* // Note: password excluded
|
|
882
|
+
* }
|
|
883
|
+
*
|
|
884
|
+
* @Injectable({ providedIn: 'root' })
|
|
885
|
+
* export class UserService extends BaseService<
|
|
886
|
+
* User,
|
|
887
|
+
* UserFilter,
|
|
888
|
+
* CreateUserDto,
|
|
889
|
+
* UpdateUserDto
|
|
890
|
+
* > {
|
|
891
|
+
* constructor(http: HttpClient) {
|
|
892
|
+
* super(http, '/api/users');
|
|
893
|
+
* }
|
|
894
|
+
* }
|
|
895
|
+
*/
|
|
479
896
|
class BaseService {
|
|
480
897
|
http;
|
|
481
898
|
baseUrl;
|
|
899
|
+
/**
|
|
900
|
+
* Creates an instance of BaseService.
|
|
901
|
+
*
|
|
902
|
+
* @param http Angular HttpClient for making HTTP requests
|
|
903
|
+
* @param baseUrl Base URL for the resource API endpoint (e.g., '/api/users')
|
|
904
|
+
*/
|
|
482
905
|
constructor(http, baseUrl) {
|
|
483
906
|
this.http = http;
|
|
484
907
|
this.baseUrl = baseUrl;
|
|
485
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Fetches a paginated list of records with optional filtering and sorting.
|
|
911
|
+
*
|
|
912
|
+
* @param filter Optional filter object containing pagination, sorting, and search criteria
|
|
913
|
+
* @returns Observable of response containing records array and pagination metadata
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* // Basic usage
|
|
917
|
+
* userService.getAll().subscribe(response => {
|
|
918
|
+
* console.log(response.data?.records);
|
|
919
|
+
* console.log(response.data?.pager.totalRecords);
|
|
920
|
+
* });
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* // With filters
|
|
924
|
+
* userService.getAll({
|
|
925
|
+
* page: 2,
|
|
926
|
+
* pageLength: 25,
|
|
927
|
+
* search: 'john',
|
|
928
|
+
* role: 'admin',
|
|
929
|
+
* sort: [new SortItem('name', 'ASC')]
|
|
930
|
+
* }).subscribe(response => {
|
|
931
|
+
* // Handle response
|
|
932
|
+
* });
|
|
933
|
+
*/
|
|
486
934
|
getAll(filter = {}) {
|
|
487
935
|
const clonedFilter = structuredClone(filter);
|
|
488
936
|
const query = !isEmpty(filter) ? jsonToQueryString(clonedFilter) : '';
|
|
489
937
|
return this.http.get(`${this.baseUrl}${query}`);
|
|
490
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* Fetches a single record by its ID.
|
|
941
|
+
*
|
|
942
|
+
* @param id The unique identifier of the record
|
|
943
|
+
* @returns Observable of response containing the single record
|
|
944
|
+
*
|
|
945
|
+
* @example
|
|
946
|
+
* userService.getDetails(123).subscribe(response => {
|
|
947
|
+
* if (response.data) {
|
|
948
|
+
* console.log('User:', response.data);
|
|
949
|
+
* }
|
|
950
|
+
* });
|
|
951
|
+
*/
|
|
491
952
|
getDetails(id) {
|
|
492
953
|
return this.http.get(`${this.baseUrl}/${id}`);
|
|
493
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Creates a new record.
|
|
957
|
+
*
|
|
958
|
+
* @param data Data transfer object containing fields for the new record
|
|
959
|
+
* @returns Observable of response containing the created record (usually with generated ID)
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* userService.create({
|
|
963
|
+
* name: 'John Doe',
|
|
964
|
+
* email: 'john@example.com',
|
|
965
|
+
* password: 'secure123'
|
|
966
|
+
* }).subscribe(response => {
|
|
967
|
+
* if (response.data) {
|
|
968
|
+
* console.log('Created user:', response.data);
|
|
969
|
+
* }
|
|
970
|
+
* });
|
|
971
|
+
*/
|
|
494
972
|
create(data) {
|
|
495
973
|
return this.http.post(this.baseUrl, data);
|
|
496
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Updates an existing record.
|
|
977
|
+
*
|
|
978
|
+
* @param id The unique identifier of the record to update
|
|
979
|
+
* @param data Data transfer object containing fields to update (partial update supported)
|
|
980
|
+
* @returns Observable of response containing the updated record
|
|
981
|
+
*
|
|
982
|
+
* @example
|
|
983
|
+
* userService.update(123, {
|
|
984
|
+
* name: 'Jane Doe',
|
|
985
|
+
* email: 'jane@example.com'
|
|
986
|
+
* }).subscribe(response => {
|
|
987
|
+
* if (response.data) {
|
|
988
|
+
* console.log('Updated user:', response.data);
|
|
989
|
+
* }
|
|
990
|
+
* });
|
|
991
|
+
*/
|
|
497
992
|
update(id, data) {
|
|
498
993
|
return this.http.put(`${this.baseUrl}/${id}`, data);
|
|
499
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Deletes a record (soft or hard delete).
|
|
997
|
+
*
|
|
998
|
+
* @param id The unique identifier of the record to delete
|
|
999
|
+
* @param method Deletion method: 'soft' (mark as deleted, reversible) or 'hard' (permanent removal)
|
|
1000
|
+
* @returns Observable of response confirming deletion
|
|
1001
|
+
*
|
|
1002
|
+
* @remarks
|
|
1003
|
+
* - Soft delete: Record is marked as deleted but can be restored later
|
|
1004
|
+
* - Hard delete: Record is permanently removed from the database
|
|
1005
|
+
* - Default is 'soft' for safety
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* // Soft delete (default)
|
|
1009
|
+
* userService.delete(123).subscribe(() => {
|
|
1010
|
+
* console.log('User soft deleted');
|
|
1011
|
+
* });
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* // Hard delete (permanent)
|
|
1015
|
+
* userService.delete(123, 'hard').subscribe(() => {
|
|
1016
|
+
* console.log('User permanently deleted');
|
|
1017
|
+
* });
|
|
1018
|
+
*/
|
|
500
1019
|
delete(id, method = 'soft') {
|
|
501
1020
|
const methodQuery = `?method=${method}`;
|
|
502
|
-
if (method === 'soft') {
|
|
503
|
-
return this.http.delete(`${this.baseUrl}/${id}${methodQuery}`, {});
|
|
504
|
-
}
|
|
505
1021
|
return this.http.delete(`${this.baseUrl}/${id}${methodQuery}`);
|
|
506
1022
|
}
|
|
507
1023
|
}
|
|
508
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Service for handling and processing API errors consistently.
|
|
1027
|
+
*
|
|
1028
|
+
* @example
|
|
1029
|
+
* // Basic usage in a service
|
|
1030
|
+
* constructor(private errorHandler: ApiErrorHandler) {}
|
|
1031
|
+
*
|
|
1032
|
+
* loadData() {
|
|
1033
|
+
* this.http.get('/api/data').subscribe({
|
|
1034
|
+
* error: (err: HttpErrorResponse) => {
|
|
1035
|
+
* const processed = this.errorHandler.handleError(err);
|
|
1036
|
+
* this.showErrorToUser(processed.message);
|
|
1037
|
+
* }
|
|
1038
|
+
* });
|
|
1039
|
+
* }
|
|
1040
|
+
*
|
|
1041
|
+
* @example
|
|
1042
|
+
* // Configure globally
|
|
1043
|
+
* providers: [
|
|
1044
|
+
* {
|
|
1045
|
+
* provide: ApiErrorHandler,
|
|
1046
|
+
* useFactory: () => {
|
|
1047
|
+
* const handler = new ApiErrorHandler();
|
|
1048
|
+
* handler.configure({
|
|
1049
|
+
* logErrors: true,
|
|
1050
|
+
* onError: (error) => {
|
|
1051
|
+
* // Send to logging service
|
|
1052
|
+
* loggingService.logError(error);
|
|
1053
|
+
* }
|
|
1054
|
+
* });
|
|
1055
|
+
* return handler;
|
|
1056
|
+
* }
|
|
1057
|
+
* }
|
|
1058
|
+
* ]
|
|
1059
|
+
*/
|
|
1060
|
+
class ApiErrorHandler {
|
|
1061
|
+
config = {
|
|
1062
|
+
logErrors: true
|
|
1063
|
+
};
|
|
1064
|
+
/**
|
|
1065
|
+
* Configure the error handler.
|
|
1066
|
+
*
|
|
1067
|
+
* @param config Configuration options
|
|
1068
|
+
*/
|
|
1069
|
+
configure(config) {
|
|
1070
|
+
this.config = { ...this.config, ...config };
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Process an HTTP error and return structured error information.
|
|
1074
|
+
*
|
|
1075
|
+
* @param error The HTTP error response
|
|
1076
|
+
* @returns Processed error with user-friendly message and metadata
|
|
1077
|
+
*/
|
|
1078
|
+
handleError(error) {
|
|
1079
|
+
const processed = this.processError(error);
|
|
1080
|
+
if (this.config.logErrors) {
|
|
1081
|
+
this.logError(processed);
|
|
1082
|
+
}
|
|
1083
|
+
if (this.config.onError) {
|
|
1084
|
+
this.config.onError(processed);
|
|
1085
|
+
}
|
|
1086
|
+
return processed;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Extract error message from various error formats.
|
|
1090
|
+
*
|
|
1091
|
+
* @param error The HTTP error response
|
|
1092
|
+
* @returns User-friendly error message
|
|
1093
|
+
*/
|
|
1094
|
+
extractErrorMessage(error) {
|
|
1095
|
+
if (this.config.errorMessageTransformer) {
|
|
1096
|
+
return this.config.errorMessageTransformer(error);
|
|
1097
|
+
}
|
|
1098
|
+
// Try to extract from API response
|
|
1099
|
+
if (error.error && typeof error.error === 'object') {
|
|
1100
|
+
const apiError = error.error;
|
|
1101
|
+
// Check for nested error object
|
|
1102
|
+
if (apiError.error?.message) {
|
|
1103
|
+
return apiError.error.message;
|
|
1104
|
+
}
|
|
1105
|
+
// Check for direct message
|
|
1106
|
+
if (apiError.message) {
|
|
1107
|
+
return apiError.message;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Fallback to status-based messages
|
|
1111
|
+
return this.getDefaultMessageForStatus(error.status);
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Get default error message based on HTTP status code.
|
|
1115
|
+
*
|
|
1116
|
+
* @param status HTTP status code
|
|
1117
|
+
* @returns Default error message
|
|
1118
|
+
*/
|
|
1119
|
+
getDefaultMessageForStatus(status) {
|
|
1120
|
+
switch (status) {
|
|
1121
|
+
case 0:
|
|
1122
|
+
return 'Unable to connect to the server. Please check your internet connection.';
|
|
1123
|
+
case 400:
|
|
1124
|
+
return 'Invalid request. Please check your input and try again.';
|
|
1125
|
+
case 401:
|
|
1126
|
+
return 'You are not authorized. Please log in and try again.';
|
|
1127
|
+
case 403:
|
|
1128
|
+
return 'You do not have permission to perform this action.';
|
|
1129
|
+
case 404:
|
|
1130
|
+
return 'The requested resource was not found.';
|
|
1131
|
+
case 408:
|
|
1132
|
+
return 'Request timeout. Please try again.';
|
|
1133
|
+
case 409:
|
|
1134
|
+
return 'This action conflicts with the current state. Please refresh and try again.';
|
|
1135
|
+
case 422:
|
|
1136
|
+
return 'Validation failed. Please check your input.';
|
|
1137
|
+
case 429:
|
|
1138
|
+
return 'Too many requests. Please wait a moment and try again.';
|
|
1139
|
+
case 500:
|
|
1140
|
+
return 'An internal server error occurred. Please try again later.';
|
|
1141
|
+
case 502:
|
|
1142
|
+
return 'Bad gateway. The server is temporarily unavailable.';
|
|
1143
|
+
case 503:
|
|
1144
|
+
return 'Service unavailable. Please try again later.';
|
|
1145
|
+
case 504:
|
|
1146
|
+
return 'Gateway timeout. The server took too long to respond.';
|
|
1147
|
+
default:
|
|
1148
|
+
if (status >= 400 && status < 500) {
|
|
1149
|
+
return 'Client error occurred. Please check your request.';
|
|
1150
|
+
}
|
|
1151
|
+
else if (status >= 500) {
|
|
1152
|
+
return 'Server error occurred. Please try again later.';
|
|
1153
|
+
}
|
|
1154
|
+
return 'An unexpected error occurred. Please try again.';
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Classify error type based on status code.
|
|
1159
|
+
*
|
|
1160
|
+
* @param status HTTP status code
|
|
1161
|
+
* @returns Error type classification
|
|
1162
|
+
*/
|
|
1163
|
+
getErrorType(status) {
|
|
1164
|
+
if (status === 0) {
|
|
1165
|
+
return 'network';
|
|
1166
|
+
}
|
|
1167
|
+
else if (status >= 400 && status < 500) {
|
|
1168
|
+
return 'client';
|
|
1169
|
+
}
|
|
1170
|
+
else if (status >= 500) {
|
|
1171
|
+
return 'server';
|
|
1172
|
+
}
|
|
1173
|
+
return 'unknown';
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Determine if error should be shown to user.
|
|
1177
|
+
*
|
|
1178
|
+
* @param error HTTP error response
|
|
1179
|
+
* @returns Whether to show error to user
|
|
1180
|
+
*/
|
|
1181
|
+
shouldShowToUser(error) {
|
|
1182
|
+
// Check if API explicitly set showMessageToUser flag
|
|
1183
|
+
if (error.error?.error?.showMessageToUser !== undefined) {
|
|
1184
|
+
return error.error.error.showMessageToUser;
|
|
1185
|
+
}
|
|
1186
|
+
// By default, show client errors (4xx) but not server errors (5xx)
|
|
1187
|
+
// unless it's a network error (0)
|
|
1188
|
+
const status = error.status;
|
|
1189
|
+
return status === 0 || (status >= 400 && status < 500);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Process the error into a structured format.
|
|
1193
|
+
*
|
|
1194
|
+
* @param error HTTP error response
|
|
1195
|
+
* @returns Processed error object
|
|
1196
|
+
*/
|
|
1197
|
+
processError(error) {
|
|
1198
|
+
const message = this.extractErrorMessage(error);
|
|
1199
|
+
const type = this.getErrorType(error.status);
|
|
1200
|
+
const showToUser = this.shouldShowToUser(error);
|
|
1201
|
+
let details;
|
|
1202
|
+
let code;
|
|
1203
|
+
// Extract additional details if available
|
|
1204
|
+
if (error.error && typeof error.error === 'object') {
|
|
1205
|
+
const apiError = error.error;
|
|
1206
|
+
if (apiError.error) {
|
|
1207
|
+
details = apiError.error.details;
|
|
1208
|
+
code = apiError.error.code;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
status: error.status,
|
|
1213
|
+
message,
|
|
1214
|
+
details,
|
|
1215
|
+
code,
|
|
1216
|
+
originalError: error,
|
|
1217
|
+
showToUser,
|
|
1218
|
+
type
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Log error to console (in dev mode).
|
|
1223
|
+
*
|
|
1224
|
+
* @param error Processed error
|
|
1225
|
+
*/
|
|
1226
|
+
logError(error) {
|
|
1227
|
+
const style = 'color: #ff6b6b; font-weight: bold;';
|
|
1228
|
+
console.group('%c🔥 API Error', style);
|
|
1229
|
+
console.log('Status:', error.status);
|
|
1230
|
+
console.log('Type:', error.type);
|
|
1231
|
+
console.log('Message:', error.message);
|
|
1232
|
+
if (error.code) {
|
|
1233
|
+
console.log('Code:', error.code);
|
|
1234
|
+
}
|
|
1235
|
+
if (error.details) {
|
|
1236
|
+
console.log('Details:', error.details);
|
|
1237
|
+
}
|
|
1238
|
+
console.log('URL:', error.originalError.url);
|
|
1239
|
+
console.log('Method:', error.originalError.error?.method || 'Unknown');
|
|
1240
|
+
console.log('Original Error:', error.originalError);
|
|
1241
|
+
console.groupEnd();
|
|
1242
|
+
}
|
|
1243
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1244
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, providedIn: 'root' });
|
|
1245
|
+
}
|
|
1246
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, decorators: [{
|
|
1247
|
+
type: Injectable,
|
|
1248
|
+
args: [{ providedIn: 'root' }]
|
|
1249
|
+
}] });
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* HTTP Interceptor that catches errors and processes them through the ApiErrorHandler.
|
|
1253
|
+
*
|
|
1254
|
+
* @example
|
|
1255
|
+
* // In your app.config.ts or main provider:
|
|
1256
|
+
* export const appConfig: ApplicationConfig = {
|
|
1257
|
+
* providers: [
|
|
1258
|
+
* provideHttpClient(
|
|
1259
|
+
* withInterceptors([apiErrorInterceptor])
|
|
1260
|
+
* )
|
|
1261
|
+
* ]
|
|
1262
|
+
* };
|
|
1263
|
+
*/
|
|
1264
|
+
const apiErrorInterceptor = (req, next) => {
|
|
1265
|
+
const errorHandler = inject(ApiErrorHandler);
|
|
1266
|
+
return next(req).pipe(catchError((error) => {
|
|
1267
|
+
const handledError = errorHandler.handleError(error);
|
|
1268
|
+
return throwError(() => handledError);
|
|
1269
|
+
}));
|
|
1270
|
+
};
|
|
1271
|
+
|
|
509
1272
|
/*
|
|
510
1273
|
* Public API Surface of api-core
|
|
511
1274
|
*/
|
|
@@ -514,5 +1277,5 @@ class BaseService {
|
|
|
514
1277
|
* Generated bundle index. Do not edit.
|
|
515
1278
|
*/
|
|
516
1279
|
|
|
517
|
-
export { ApiCore, BaseResourceConfig, BaseService, BaseStateService, Filter, SELECT_MODE, SortItem, generatePermissions, isEmpty, jsonToQueryString, sortObjectToString };
|
|
1280
|
+
export { API_CORE_CONFIG, ApiCore, ApiErrorHandler, BaseResourceConfig, BaseService, BaseStateService, DEFAULT_API_CORE_CONFIG, Filter, SELECT_MODE, SortItem, apiErrorInterceptor, generatePermissions, isEmpty, jsonToQueryString, mergeConfig, sortObjectToString };
|
|
518
1281
|
//# sourceMappingURL=mysteryinfosolutions-api-core.mjs.map
|