@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.
@@ -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
- 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 (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
- return !obj || Object.keys(obj).length === 0;
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
- ids = [];
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 = 1;
200
- pageLength = 10;
201
- sort = [];
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
- * A generic state management service for handling filters, pagination, and records.
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
- /** 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();
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
- * 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") {
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
- // 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);
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
- sortItems.push(new SortItem(column, sort));
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