@mysteryinfosolutions/api-core 1.7.13 → 1.9.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.
@@ -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,169 @@ 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
- const jsonToQueryString = (json) => {
173
- if (!json || typeof json !== 'object')
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
- if (json.hasOwnProperty('sort')) {
176
- json.sort = sortObjectToString(json.sort);
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(json)
179
- .filter(key => json[key] !== undefined && json[key] !== null)
318
+ const queryArray = Object.keys(params)
319
+ .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
180
320
  .map(key => {
181
- if (Array.isArray(json[key])) {
182
- return `${encodeURIComponent(key)}=${encodeURIComponent('[' + json[key].toString() + ']')}`;
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(json[key])}`;
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
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
+ */
190
345
  const isEmpty = (obj) => {
191
346
  return !obj || Object.keys(obj).length === 0;
192
347
  };
193
348
 
349
+ /**
350
+ * Base filter class providing common filtering, pagination, and sorting capabilities.
351
+ *
352
+ * @remarks
353
+ * Extend this class in your custom filter types to add domain-specific filter properties.
354
+ * The base filter handles pagination, sorting, searching, and date range filtering.
355
+ *
356
+ * @example
357
+ * interface UserFilter extends Filter {
358
+ * role?: string;
359
+ * isActive?: boolean;
360
+ * departmentId?: number;
361
+ * }
362
+ *
363
+ * const filter: UserFilter = {
364
+ * page: 1,
365
+ * pageLength: 25,
366
+ * search: 'john',
367
+ * role: 'admin',
368
+ * sort: [new SortItem('name', 'ASC')]
369
+ * };
370
+ */
194
371
  class Filter {
195
- ids = [];
372
+ /** Array of specific IDs to filter by */
373
+ ids;
374
+ /** Column name to use for date range filtering */
196
375
  dateRangeColumn;
376
+ /** Start date for date range filter (ISO format) */
197
377
  dateRangeFrom;
378
+ /** End date for date range filter (ISO format) */
198
379
  dateRangeTo;
199
- page = 1;
200
- pageLength = 10;
201
- sort = [];
380
+ /** Current page number (1-based) */
381
+ page;
382
+ /** Number of records per page */
383
+ pageLength;
384
+ /** Array of sort configurations */
385
+ sort;
386
+ /** Search query string */
202
387
  search;
388
+ /** Comma-separated list of columns to search in */
203
389
  searchColumns;
390
+ /** Comma-separated list of columns to select/return */
204
391
  selectColumns;
392
+ /** Mode for column selection */
205
393
  selectMode;
206
394
  }
207
395
 
396
+ /**
397
+ * Represents a single sort configuration for a field.
398
+ *
399
+ * @remarks
400
+ * Used in filter objects to specify sorting criteria.
401
+ * Multiple SortItem instances can be combined for multi-column sorting.
402
+ *
403
+ * @example
404
+ * // Single sort
405
+ * const sort = new SortItem('name', 'ASC');
406
+ *
407
+ * @example
408
+ * // Multi-column sort
409
+ * const sorts = [
410
+ * new SortItem('priority', 'DESC'),
411
+ * new SortItem('createdAt', 'DESC'),
412
+ * new SortItem('name', 'ASC')
413
+ * ];
414
+ *
415
+ * const filter = {
416
+ * page: 1,
417
+ * pageLength: 25,
418
+ * sort: sorts
419
+ * };
420
+ */
208
421
  class SortItem {
209
422
  field;
210
423
  order;
424
+ /**
425
+ * Creates a sort configuration.
426
+ *
427
+ * @param field The field/column name to sort by
428
+ * @param order Sort direction: 'ASC' (ascending) or 'DESC' (descending)
429
+ */
211
430
  constructor(field, order) {
212
431
  this.field = field;
213
432
  this.order = order;
@@ -217,7 +436,78 @@ class SortItem {
217
436
  ;
218
437
 
219
438
  /**
220
- * A generic state management service for handling filters, pagination, and records.
439
+ * Generic reactive state management service for data-driven features.
440
+ *
441
+ * @template TRecord The type of record/entity being managed
442
+ * @template TFilter Filter type extending Partial<TRecord> & Filter
443
+ *
444
+ * @remarks
445
+ * BaseStateService provides comprehensive state management for list-based views including:
446
+ * - Filter state with pagination and sorting
447
+ * - Records collection with CRUD helpers
448
+ * - Loading states (with context keys)
449
+ * - Error handling
450
+ * - Record selection
451
+ * - Pagination metadata
452
+ *
453
+ * All state is reactive using RxJS BehaviorSubjects, making it easy to integrate
454
+ * with Angular templates using the async pipe.
455
+ *
456
+ * @example
457
+ * // Basic setup in component
458
+ * @Component({
459
+ * selector: 'app-users',
460
+ * providers: [BaseStateService] // Component-level instance
461
+ * })
462
+ * export class UsersComponent implements OnInit, OnDestroy {
463
+ * state = new BaseStateService<User, UserFilter>();
464
+ *
465
+ * constructor(private userService: UserService) {}
466
+ *
467
+ * ngOnInit() {
468
+ * // Subscribe to filter changes
469
+ * this.state.filter$.subscribe(filter => {
470
+ * this.loadUsers(filter);
471
+ * });
472
+ *
473
+ * // Set initial filter
474
+ * this.state.setFilter({ page: 1, pageLength: 25 });
475
+ * }
476
+ *
477
+ * loadUsers(filter: UserFilter) {
478
+ * this.state.setLoading('list', true);
479
+ *
480
+ * this.userService.getAll(filter).subscribe({
481
+ * next: (response) => {
482
+ * if (response.data) {
483
+ * this.state.setApiResponse(response.data);
484
+ * }
485
+ * this.state.setLoading('list', false);
486
+ * },
487
+ * error: (err) => {
488
+ * this.state.setError('Failed to load users');
489
+ * this.state.setLoading('list', false);
490
+ * }
491
+ * });
492
+ * }
493
+ *
494
+ * ngOnDestroy() {
495
+ * this.state.destroy();
496
+ * }
497
+ * }
498
+ *
499
+ * @example
500
+ * // Using in template
501
+ * <div *ngIf="state.isLoading$('list') | async">Loading...</div>
502
+ * <div *ngIf="state.error$ | async as error">{{ error }}</div>
503
+ *
504
+ * <div *ngFor="let user of state.records$ | async">
505
+ * {{ user.name }}
506
+ * </div>
507
+ *
508
+ * <div *ngIf="state.pager$ | async as pager">
509
+ * Page {{ pager.currentPage }} of {{ pager.lastPage }}
510
+ * </div>
221
511
  */
222
512
  class BaseStateService {
223
513
  filterSubject = new BehaviorSubject({ page: 1, pageLength: 10 });
@@ -226,18 +516,36 @@ class BaseStateService {
226
516
  selectedSubject = new BehaviorSubject(null);
227
517
  loadingMapSubject = new BehaviorSubject({});
228
518
  errorSubject = new BehaviorSubject(null);
229
- /** Observable stream of current filter. */
230
- filter$ = this.filterSubject.asObservable();
231
- /** Observable stream of current records. */
232
- records$ = this.recordsSubject.asObservable();
233
- /** Observable stream of current pager metadata. */
234
- pager$ = this.pagerSubject.asObservable();
235
- /** Observable stream of the currently selected record. */
236
- selected$ = this.selectedSubject.asObservable();
237
- /** Observable stream of loading state. */
238
- loading$ = this.loadingMapSubject.asObservable();
239
- /** Observable stream of current error message. */
240
- error$ = this.errorSubject.asObservable();
519
+ /**
520
+ * Observable stream of current filter.
521
+ * Optimized with distinctUntilChanged to prevent duplicate emissions.
522
+ */
523
+ filter$ = this.filterSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
524
+ /**
525
+ * Observable stream of current records.
526
+ * Optimized with distinctUntilChanged for reference equality.
527
+ */
528
+ records$ = this.recordsSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
529
+ /**
530
+ * Observable stream of current pager metadata.
531
+ * Optimized with distinctUntilChanged for deep equality.
532
+ */
533
+ pager$ = this.pagerSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
534
+ /**
535
+ * Observable stream of the currently selected record.
536
+ * Optimized with distinctUntilChanged for reference equality.
537
+ */
538
+ selected$ = this.selectedSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
539
+ /**
540
+ * Observable stream of loading state.
541
+ * Optimized with distinctUntilChanged for deep equality.
542
+ */
543
+ loading$ = this.loadingMapSubject.asObservable().pipe(distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)), shareReplay({ bufferSize: 1, refCount: true }));
544
+ /**
545
+ * Observable stream of current error message.
546
+ * Optimized with distinctUntilChanged for value equality.
547
+ */
548
+ error$ = this.errorSubject.asObservable().pipe(distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
241
549
  /** Returns the current filter. */
242
550
  get currentFilter() {
243
551
  return this.filterSubject.value;
@@ -288,24 +596,45 @@ class BaseStateService {
288
596
  this.setPager(response.pager);
289
597
  }
290
598
  /**
291
- * Updates the sort order in the current filter.
292
- * Toggles order if column already exists, or adds it otherwise.
293
- * @param column Column name to sort by.
294
- */
295
- setSort(column, sort = "ASC") {
599
+ * Updates the sort order in the current filter.
600
+ * Toggles order if column already exists and no explicit sort is provided, or adds it otherwise.
601
+ *
602
+ * @param column Column name to sort by
603
+ * @param sort Optional sort order. If not provided and column exists, toggles between ASC/DESC.
604
+ * If not provided and column doesn't exist, defaults to ASC.
605
+ *
606
+ * @example
607
+ * // Add new sort (defaults to ASC)
608
+ * state.setSort('name');
609
+ *
610
+ * @example
611
+ * // Toggle existing sort
612
+ * state.setSort('name'); // If already ASC, becomes DESC; if DESC, becomes ASC
613
+ *
614
+ * @example
615
+ * // Explicitly set sort order
616
+ * state.setSort('name', 'DESC'); // Always sets to DESC
617
+ */
618
+ setSort(column, sort) {
296
619
  const current = this.filterSubject.value;
297
620
  let sortItems = [...(current.sort ?? [])];
298
621
  const index = sortItems.findIndex(item => item.field === column);
299
622
  if (index !== -1) {
300
- // If sort is explicitly provided, set it; otherwise, toggle
301
- const currentOrder = sortItems[index].order;
302
- const newOrder = (typeof sort === 'undefined' || sort === null)
303
- ? (currentOrder === 'ASC' ? 'DESC' : 'ASC')
304
- : sort;
305
- sortItems[index] = new SortItem(sortItems[index].field, newOrder);
623
+ // Column already exists in sort
624
+ if (sort !== undefined) {
625
+ // Explicit sort provided - use it
626
+ sortItems[index] = new SortItem(sortItems[index].field, sort);
627
+ }
628
+ else {
629
+ // No explicit sort - toggle the order
630
+ const currentOrder = sortItems[index].order;
631
+ const newOrder = currentOrder === 'ASC' ? 'DESC' : 'ASC';
632
+ sortItems[index] = new SortItem(sortItems[index].field, newOrder);
633
+ }
306
634
  }
307
635
  else {
308
- sortItems.push(new SortItem(column, sort));
636
+ // Column doesn't exist - add it (default to ASC if not specified)
637
+ sortItems.push(new SortItem(column, sort ?? 'ASC'));
309
638
  }
310
639
  this.filterSubject.next({
311
640
  ...current,
@@ -340,9 +669,13 @@ class BaseStateService {
340
669
  *
341
670
  * @param key A unique key representing the loading context (e.g., "list", "detail")
342
671
  * @returns Observable emitting `true` if the given context is loading, `false` otherwise.
672
+ *
673
+ * @remarks
674
+ * Optimized with distinctUntilChanged to prevent duplicate emissions
675
+ * and shareReplay to share subscriptions.
343
676
  */
344
677
  isLoading$(key) {
345
- return this.loading$.pipe(map(state => !!state[key]));
678
+ return this.loading$.pipe(map(state => !!state[key]), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
346
679
  }
347
680
  /**
348
681
  * Sets the loading state for a specific key.
@@ -455,6 +788,7 @@ class BaseStateService {
455
788
  /** Resets and clears all managed state. */
456
789
  destroy() {
457
790
  this.reset();
791
+ this.destroySubscriptions();
458
792
  }
459
793
  /**
460
794
  * Completes specified subjects managed by this service.
@@ -475,36 +809,452 @@ class BaseStateService {
475
809
  }
476
810
  }
477
811
 
812
+ /**
813
+ * Abstract base service providing standard CRUD operations for REST APIs.
814
+ *
815
+ * @template T The full model type representing your entity
816
+ * @template TFilter Filter type extending Partial<T> & Filter for query parameters
817
+ * @template TCreate DTO type for creating new records (defaults to Partial<T>)
818
+ * @template TUpdate DTO type for updating records (defaults to Partial<T>)
819
+ *
820
+ * @remarks
821
+ * Extend this class in your feature services to get type-safe CRUD operations
822
+ * with minimal boilerplate. The service automatically handles:
823
+ * - Query string generation from filters
824
+ * - Pagination and sorting
825
+ * - Standardized response/error handling
826
+ * - Type safety throughout the request/response cycle
827
+ *
828
+ * @example
829
+ * // Define your model
830
+ * interface User {
831
+ * id: number;
832
+ * name: string;
833
+ * email: string;
834
+ * role: string;
835
+ * }
836
+ *
837
+ * // Define custom filter
838
+ * interface UserFilter extends Filter {
839
+ * role?: string;
840
+ * isActive?: boolean;
841
+ * }
842
+ *
843
+ * // Create service
844
+ * @Injectable({ providedIn: 'root' })
845
+ * export class UserService extends BaseService<User, UserFilter> {
846
+ * constructor(http: HttpClient) {
847
+ * super(http, '/api/users');
848
+ * }
849
+ *
850
+ * // Add custom methods
851
+ * activateUser(id: number): Observable<IResponse<User>> {
852
+ * return this.http.post<IResponse<User>>(`${this.baseUrl}/${id}/activate`, {});
853
+ * }
854
+ * }
855
+ *
856
+ * @example
857
+ * // With separate DTOs for create/update
858
+ * interface CreateUserDto {
859
+ * name: string;
860
+ * email: string;
861
+ * password: string;
862
+ * }
863
+ *
864
+ * interface UpdateUserDto {
865
+ * name?: string;
866
+ * email?: string;
867
+ * // Note: password excluded
868
+ * }
869
+ *
870
+ * @Injectable({ providedIn: 'root' })
871
+ * export class UserService extends BaseService<
872
+ * User,
873
+ * UserFilter,
874
+ * CreateUserDto,
875
+ * UpdateUserDto
876
+ * > {
877
+ * constructor(http: HttpClient) {
878
+ * super(http, '/api/users');
879
+ * }
880
+ * }
881
+ */
478
882
  class BaseService {
479
883
  http;
480
884
  baseUrl;
885
+ /**
886
+ * Creates an instance of BaseService.
887
+ *
888
+ * @param http Angular HttpClient for making HTTP requests
889
+ * @param baseUrl Base URL for the resource API endpoint (e.g., '/api/users')
890
+ */
481
891
  constructor(http, baseUrl) {
482
892
  this.http = http;
483
893
  this.baseUrl = baseUrl;
484
894
  }
895
+ /**
896
+ * Fetches a paginated list of records with optional filtering and sorting.
897
+ *
898
+ * @param filter Optional filter object containing pagination, sorting, and search criteria
899
+ * @returns Observable of response containing records array and pagination metadata
900
+ *
901
+ * @example
902
+ * // Basic usage
903
+ * userService.getAll().subscribe(response => {
904
+ * console.log(response.data?.records);
905
+ * console.log(response.data?.pager.totalRecords);
906
+ * });
907
+ *
908
+ * @example
909
+ * // With filters
910
+ * userService.getAll({
911
+ * page: 2,
912
+ * pageLength: 25,
913
+ * search: 'john',
914
+ * role: 'admin',
915
+ * sort: [new SortItem('name', 'ASC')]
916
+ * }).subscribe(response => {
917
+ * // Handle response
918
+ * });
919
+ */
485
920
  getAll(filter = {}) {
486
921
  const clonedFilter = structuredClone(filter);
487
922
  const query = !isEmpty(filter) ? jsonToQueryString(clonedFilter) : '';
488
923
  return this.http.get(`${this.baseUrl}${query}`);
489
924
  }
925
+ /**
926
+ * Fetches a single record by its ID.
927
+ *
928
+ * @param id The unique identifier of the record
929
+ * @returns Observable of response containing the single record
930
+ *
931
+ * @example
932
+ * userService.getDetails(123).subscribe(response => {
933
+ * if (response.data) {
934
+ * console.log('User:', response.data);
935
+ * }
936
+ * });
937
+ */
490
938
  getDetails(id) {
491
939
  return this.http.get(`${this.baseUrl}/${id}`);
492
940
  }
941
+ /**
942
+ * Creates a new record.
943
+ *
944
+ * @param data Data transfer object containing fields for the new record
945
+ * @returns Observable of response containing the created record (usually with generated ID)
946
+ *
947
+ * @example
948
+ * userService.create({
949
+ * name: 'John Doe',
950
+ * email: 'john@example.com',
951
+ * password: 'secure123'
952
+ * }).subscribe(response => {
953
+ * if (response.data) {
954
+ * console.log('Created user:', response.data);
955
+ * }
956
+ * });
957
+ */
493
958
  create(data) {
494
959
  return this.http.post(this.baseUrl, data);
495
960
  }
961
+ /**
962
+ * Updates an existing record.
963
+ *
964
+ * @param id The unique identifier of the record to update
965
+ * @param data Data transfer object containing fields to update (partial update supported)
966
+ * @returns Observable of response containing the updated record
967
+ *
968
+ * @example
969
+ * userService.update(123, {
970
+ * name: 'Jane Doe',
971
+ * email: 'jane@example.com'
972
+ * }).subscribe(response => {
973
+ * if (response.data) {
974
+ * console.log('Updated user:', response.data);
975
+ * }
976
+ * });
977
+ */
496
978
  update(id, data) {
497
979
  return this.http.put(`${this.baseUrl}/${id}`, data);
498
980
  }
981
+ /**
982
+ * Deletes a record (soft or hard delete).
983
+ *
984
+ * @param id The unique identifier of the record to delete
985
+ * @param method Deletion method: 'soft' (mark as deleted, reversible) or 'hard' (permanent removal)
986
+ * @returns Observable of response confirming deletion
987
+ *
988
+ * @remarks
989
+ * - Soft delete: Record is marked as deleted but can be restored later
990
+ * - Hard delete: Record is permanently removed from the database
991
+ * - Default is 'soft' for safety
992
+ *
993
+ * @example
994
+ * // Soft delete (default)
995
+ * userService.delete(123).subscribe(() => {
996
+ * console.log('User soft deleted');
997
+ * });
998
+ *
999
+ * @example
1000
+ * // Hard delete (permanent)
1001
+ * userService.delete(123, 'hard').subscribe(() => {
1002
+ * console.log('User permanently deleted');
1003
+ * });
1004
+ */
499
1005
  delete(id, method = 'soft') {
500
1006
  const methodQuery = `?method=${method}`;
501
- if (method === 'soft') {
502
- return this.http.delete(`${this.baseUrl}/${id}${methodQuery}`, {});
503
- }
504
1007
  return this.http.delete(`${this.baseUrl}/${id}${methodQuery}`);
505
1008
  }
506
1009
  }
507
1010
 
1011
+ /**
1012
+ * Service for handling and processing API errors consistently.
1013
+ *
1014
+ * @example
1015
+ * // Basic usage in a service
1016
+ * constructor(private errorHandler: ApiErrorHandler) {}
1017
+ *
1018
+ * loadData() {
1019
+ * this.http.get('/api/data').subscribe({
1020
+ * error: (err: HttpErrorResponse) => {
1021
+ * const processed = this.errorHandler.handleError(err);
1022
+ * this.showErrorToUser(processed.message);
1023
+ * }
1024
+ * });
1025
+ * }
1026
+ *
1027
+ * @example
1028
+ * // Configure globally
1029
+ * providers: [
1030
+ * {
1031
+ * provide: ApiErrorHandler,
1032
+ * useFactory: () => {
1033
+ * const handler = new ApiErrorHandler();
1034
+ * handler.configure({
1035
+ * logErrors: true,
1036
+ * onError: (error) => {
1037
+ * // Send to logging service
1038
+ * loggingService.logError(error);
1039
+ * }
1040
+ * });
1041
+ * return handler;
1042
+ * }
1043
+ * }
1044
+ * ]
1045
+ */
1046
+ class ApiErrorHandler {
1047
+ config = {
1048
+ logErrors: true
1049
+ };
1050
+ /**
1051
+ * Configure the error handler.
1052
+ *
1053
+ * @param config Configuration options
1054
+ */
1055
+ configure(config) {
1056
+ this.config = { ...this.config, ...config };
1057
+ }
1058
+ /**
1059
+ * Process an HTTP error and return structured error information.
1060
+ *
1061
+ * @param error The HTTP error response
1062
+ * @returns Processed error with user-friendly message and metadata
1063
+ */
1064
+ handleError(error) {
1065
+ const processed = this.processError(error);
1066
+ if (this.config.logErrors) {
1067
+ this.logError(processed);
1068
+ }
1069
+ if (this.config.onError) {
1070
+ this.config.onError(processed);
1071
+ }
1072
+ return processed;
1073
+ }
1074
+ /**
1075
+ * Extract error message from various error formats.
1076
+ *
1077
+ * @param error The HTTP error response
1078
+ * @returns User-friendly error message
1079
+ */
1080
+ extractErrorMessage(error) {
1081
+ if (this.config.errorMessageTransformer) {
1082
+ return this.config.errorMessageTransformer(error);
1083
+ }
1084
+ // Try to extract from API response
1085
+ if (error.error && typeof error.error === 'object') {
1086
+ const apiError = error.error;
1087
+ // Check for nested error object
1088
+ if (apiError.error?.message) {
1089
+ return apiError.error.message;
1090
+ }
1091
+ // Check for direct message
1092
+ if (apiError.message) {
1093
+ return apiError.message;
1094
+ }
1095
+ }
1096
+ // Fallback to status-based messages
1097
+ return this.getDefaultMessageForStatus(error.status);
1098
+ }
1099
+ /**
1100
+ * Get default error message based on HTTP status code.
1101
+ *
1102
+ * @param status HTTP status code
1103
+ * @returns Default error message
1104
+ */
1105
+ getDefaultMessageForStatus(status) {
1106
+ switch (status) {
1107
+ case 0:
1108
+ return 'Unable to connect to the server. Please check your internet connection.';
1109
+ case 400:
1110
+ return 'Invalid request. Please check your input and try again.';
1111
+ case 401:
1112
+ return 'You are not authorized. Please log in and try again.';
1113
+ case 403:
1114
+ return 'You do not have permission to perform this action.';
1115
+ case 404:
1116
+ return 'The requested resource was not found.';
1117
+ case 408:
1118
+ return 'Request timeout. Please try again.';
1119
+ case 409:
1120
+ return 'This action conflicts with the current state. Please refresh and try again.';
1121
+ case 422:
1122
+ return 'Validation failed. Please check your input.';
1123
+ case 429:
1124
+ return 'Too many requests. Please wait a moment and try again.';
1125
+ case 500:
1126
+ return 'An internal server error occurred. Please try again later.';
1127
+ case 502:
1128
+ return 'Bad gateway. The server is temporarily unavailable.';
1129
+ case 503:
1130
+ return 'Service unavailable. Please try again later.';
1131
+ case 504:
1132
+ return 'Gateway timeout. The server took too long to respond.';
1133
+ default:
1134
+ if (status >= 400 && status < 500) {
1135
+ return 'Client error occurred. Please check your request.';
1136
+ }
1137
+ else if (status >= 500) {
1138
+ return 'Server error occurred. Please try again later.';
1139
+ }
1140
+ return 'An unexpected error occurred. Please try again.';
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Classify error type based on status code.
1145
+ *
1146
+ * @param status HTTP status code
1147
+ * @returns Error type classification
1148
+ */
1149
+ getErrorType(status) {
1150
+ if (status === 0) {
1151
+ return 'network';
1152
+ }
1153
+ else if (status >= 400 && status < 500) {
1154
+ return 'client';
1155
+ }
1156
+ else if (status >= 500) {
1157
+ return 'server';
1158
+ }
1159
+ return 'unknown';
1160
+ }
1161
+ /**
1162
+ * Determine if error should be shown to user.
1163
+ *
1164
+ * @param error HTTP error response
1165
+ * @returns Whether to show error to user
1166
+ */
1167
+ shouldShowToUser(error) {
1168
+ // Check if API explicitly set showMessageToUser flag
1169
+ if (error.error?.error?.showMessageToUser !== undefined) {
1170
+ return error.error.error.showMessageToUser;
1171
+ }
1172
+ // By default, show client errors (4xx) but not server errors (5xx)
1173
+ // unless it's a network error (0)
1174
+ const status = error.status;
1175
+ return status === 0 || (status >= 400 && status < 500);
1176
+ }
1177
+ /**
1178
+ * Process the error into a structured format.
1179
+ *
1180
+ * @param error HTTP error response
1181
+ * @returns Processed error object
1182
+ */
1183
+ processError(error) {
1184
+ const message = this.extractErrorMessage(error);
1185
+ const type = this.getErrorType(error.status);
1186
+ const showToUser = this.shouldShowToUser(error);
1187
+ let details;
1188
+ let code;
1189
+ // Extract additional details if available
1190
+ if (error.error && typeof error.error === 'object') {
1191
+ const apiError = error.error;
1192
+ if (apiError.error) {
1193
+ details = apiError.error.details;
1194
+ code = apiError.error.code;
1195
+ }
1196
+ }
1197
+ return {
1198
+ status: error.status,
1199
+ message,
1200
+ details,
1201
+ code,
1202
+ originalError: error,
1203
+ showToUser,
1204
+ type
1205
+ };
1206
+ }
1207
+ /**
1208
+ * Log error to console (in dev mode).
1209
+ *
1210
+ * @param error Processed error
1211
+ */
1212
+ logError(error) {
1213
+ const style = 'color: #ff6b6b; font-weight: bold;';
1214
+ console.group('%c🔥 API Error', style);
1215
+ console.log('Status:', error.status);
1216
+ console.log('Type:', error.type);
1217
+ console.log('Message:', error.message);
1218
+ if (error.code) {
1219
+ console.log('Code:', error.code);
1220
+ }
1221
+ if (error.details) {
1222
+ console.log('Details:', error.details);
1223
+ }
1224
+ console.log('URL:', error.originalError.url);
1225
+ console.log('Method:', error.originalError.error?.method || 'Unknown');
1226
+ console.log('Original Error:', error.originalError);
1227
+ console.groupEnd();
1228
+ }
1229
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1230
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, providedIn: 'root' });
1231
+ }
1232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: ApiErrorHandler, decorators: [{
1233
+ type: Injectable,
1234
+ args: [{ providedIn: 'root' }]
1235
+ }] });
1236
+
1237
+ /**
1238
+ * HTTP Interceptor that catches errors and processes them through the ApiErrorHandler.
1239
+ *
1240
+ * @example
1241
+ * // In your app.config.ts or main provider:
1242
+ * export const appConfig: ApplicationConfig = {
1243
+ * providers: [
1244
+ * provideHttpClient(
1245
+ * withInterceptors([apiErrorInterceptor])
1246
+ * )
1247
+ * ]
1248
+ * };
1249
+ */
1250
+ const apiErrorInterceptor = (req, next) => {
1251
+ const errorHandler = inject(ApiErrorHandler);
1252
+ return next(req).pipe(catchError((error) => {
1253
+ const handledError = errorHandler.handleError(error);
1254
+ return throwError(() => handledError);
1255
+ }));
1256
+ };
1257
+
508
1258
  /*
509
1259
  * Public API Surface of api-core
510
1260
  */
@@ -513,5 +1263,5 @@ class BaseService {
513
1263
  * Generated bundle index. Do not edit.
514
1264
  */
515
1265
 
516
- export { ApiCore, BaseResourceConfig, BaseService, BaseStateService, Filter, SELECT_MODE, SortItem, generatePermissions, isEmpty, jsonToQueryString, sortObjectToString };
1266
+ export { API_CORE_CONFIG, ApiCore, ApiErrorHandler, BaseResourceConfig, BaseService, BaseStateService, DEFAULT_API_CORE_CONFIG, Filter, SELECT_MODE, SortItem, apiErrorInterceptor, generatePermissions, isEmpty, jsonToQueryString, mergeConfig, sortObjectToString };
517
1267
  //# sourceMappingURL=mysteryinfosolutions-api-core.mjs.map