@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
package/README.md
CHANGED
|
@@ -1,63 +1,960 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mysteryinfosolutions/api-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive Angular library providing robust base services, state management, and utilities for building data-driven applications with RESTful APIs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://angular.io/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## 📦 Installation
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
|
|
12
|
+
npm install @mysteryinfosolutions/api-core
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
### Peer Dependencies
|
|
16
|
+
|
|
17
|
+
Ensure you have the following peer dependencies installed:
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
|
-
|
|
20
|
+
npm install @angular/common@^20.0.0 @angular/core@^20.0.0 rxjs@^7.0.0
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
##
|
|
23
|
+
## 🎯 Features
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
- ✅ **Generic CRUD Service** - Type-safe base service for REST operations
|
|
26
|
+
- ✅ **State Management** - Comprehensive reactive state management with RxJS
|
|
27
|
+
- ✅ **Query Builder** - Convert filters to query strings automatically
|
|
28
|
+
- ✅ **Pagination & Sorting** - Built-in support with metadata
|
|
29
|
+
- ✅ **Permission System** - Generate and manage resource permissions
|
|
30
|
+
- ✅ **Table Configuration** - Standardized table/grid configurations
|
|
31
|
+
- ✅ **Type-Safe Models** - Interfaces for API responses and filters
|
|
32
|
+
- ✅ **Loading States** - Context-aware loading state management
|
|
33
|
+
- ✅ **Tree-Shakable** - Optimized bundle size
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🚀 Quick Start
|
|
38
|
+
|
|
39
|
+
### 1. Basic Service Example
|
|
40
|
+
|
|
41
|
+
Create a service for your resource using `BaseService`:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Injectable } from '@angular/core';
|
|
45
|
+
import { HttpClient } from '@angular/common/http';
|
|
46
|
+
import { BaseService } from '@mysteryinfosolutions/api-core';
|
|
47
|
+
|
|
48
|
+
// Define your model
|
|
49
|
+
interface User {
|
|
50
|
+
id: number;
|
|
51
|
+
name: string;
|
|
52
|
+
email: string;
|
|
53
|
+
role: string;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Define your filter (optional custom fields)
|
|
58
|
+
interface UserFilter {
|
|
59
|
+
role?: string;
|
|
60
|
+
active?: boolean;
|
|
61
|
+
page?: number;
|
|
62
|
+
pageLength?: number;
|
|
63
|
+
search?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Injectable({ providedIn: 'root' })
|
|
67
|
+
export class UserService extends BaseService<User, UserFilter> {
|
|
68
|
+
constructor(http: HttpClient) {
|
|
69
|
+
super(http, '/api/users');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
25
72
|
```
|
|
26
73
|
|
|
27
|
-
|
|
74
|
+
### 2. Use the Service in a Component
|
|
28
75
|
|
|
29
|
-
|
|
76
|
+
```typescript
|
|
77
|
+
import { Component, OnInit } from '@angular/core';
|
|
78
|
+
import { UserService } from './user.service';
|
|
30
79
|
|
|
31
|
-
|
|
80
|
+
@Component({
|
|
81
|
+
selector: 'app-users',
|
|
82
|
+
template: `
|
|
83
|
+
<div *ngIf="users$ | async as users">
|
|
84
|
+
<div *ngFor="let user of users.data?.records">
|
|
85
|
+
{{ user.name }} - {{ user.email }}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
`
|
|
89
|
+
})
|
|
90
|
+
export class UsersComponent implements OnInit {
|
|
91
|
+
users$ = this.userService.getAll({ page: 1, pageLength: 10 });
|
|
32
92
|
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
cd dist/api-core
|
|
36
|
-
```
|
|
93
|
+
constructor(private userService: UserService) {}
|
|
37
94
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
ngOnInit() {
|
|
96
|
+
// Load users on init
|
|
97
|
+
this.loadUsers();
|
|
98
|
+
}
|
|
42
99
|
|
|
43
|
-
|
|
100
|
+
loadUsers() {
|
|
101
|
+
this.userService.getAll({
|
|
102
|
+
page: 1,
|
|
103
|
+
pageLength: 25,
|
|
104
|
+
role: 'admin',
|
|
105
|
+
search: 'john'
|
|
106
|
+
}).subscribe(response => {
|
|
107
|
+
if (response.data) {
|
|
108
|
+
console.log('Users:', response.data.records);
|
|
109
|
+
console.log('Total:', response.data.pager.totalRecords);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
44
113
|
|
|
45
|
-
|
|
114
|
+
createUser() {
|
|
115
|
+
this.userService.create({
|
|
116
|
+
name: 'John Doe',
|
|
117
|
+
email: 'john@example.com',
|
|
118
|
+
role: 'user'
|
|
119
|
+
}).subscribe(response => {
|
|
120
|
+
console.log('Created:', response.data);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
46
123
|
|
|
47
|
-
|
|
48
|
-
|
|
124
|
+
updateUser(id: number) {
|
|
125
|
+
this.userService.update(id, {
|
|
126
|
+
name: 'Jane Doe'
|
|
127
|
+
}).subscribe(response => {
|
|
128
|
+
console.log('Updated:', response.data);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
deleteUser(id: number) {
|
|
133
|
+
// Soft delete (default)
|
|
134
|
+
this.userService.delete(id).subscribe(() => {
|
|
135
|
+
console.log('Deleted');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Hard delete
|
|
139
|
+
this.userService.delete(id, 'hard').subscribe(() => {
|
|
140
|
+
console.log('Permanently deleted');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
49
144
|
```
|
|
50
145
|
|
|
51
|
-
|
|
146
|
+
---
|
|
52
147
|
|
|
53
|
-
|
|
148
|
+
## 📚 Core Components
|
|
54
149
|
|
|
55
|
-
|
|
56
|
-
|
|
150
|
+
### BaseService<T, TFilter, TCreate, TUpdate>
|
|
151
|
+
|
|
152
|
+
Generic HTTP service providing CRUD operations.
|
|
153
|
+
|
|
154
|
+
#### Type Parameters
|
|
155
|
+
|
|
156
|
+
| Parameter | Description | Default |
|
|
157
|
+
|-----------|-------------|---------|
|
|
158
|
+
| `T` | The full model type | Required |
|
|
159
|
+
| `TFilter` | Filter type (extends `Partial<T> & Filter`) | `Partial<T> & Filter` |
|
|
160
|
+
| `TCreate` | DTO for creating records | `Partial<T>` |
|
|
161
|
+
| `TUpdate` | DTO for updating records | `Partial<T>` |
|
|
162
|
+
|
|
163
|
+
#### Methods
|
|
164
|
+
|
|
165
|
+
| Method | Parameters | Returns | Description |
|
|
166
|
+
|--------|-----------|---------|-------------|
|
|
167
|
+
| `getAll()` | `filter?: TFilter` | `Observable<IResponse<IMultiresult<T>>>` | Fetch paginated list |
|
|
168
|
+
| `getDetails()` | `id: number` | `Observable<IResponse<T>>` | Fetch single record |
|
|
169
|
+
| `create()` | `data: TCreate` | `Observable<IResponse<T>>` | Create new record |
|
|
170
|
+
| `update()` | `id: number, data: TUpdate` | `Observable<IResponse<T>>` | Update existing record |
|
|
171
|
+
| `delete()` | `id: number, method?: 'soft' \| 'hard'` | `Observable<IResponse<any>>` | Delete record |
|
|
172
|
+
|
|
173
|
+
#### Example with Custom DTOs
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface User {
|
|
177
|
+
id: number;
|
|
178
|
+
name: string;
|
|
179
|
+
email: string;
|
|
180
|
+
password: string;
|
|
181
|
+
role: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface CreateUserDto {
|
|
185
|
+
name: string;
|
|
186
|
+
email: string;
|
|
187
|
+
password: string;
|
|
188
|
+
role: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface UpdateUserDto {
|
|
192
|
+
name?: string;
|
|
193
|
+
email?: string;
|
|
194
|
+
role?: string;
|
|
195
|
+
// Note: password excluded for updates
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Injectable({ providedIn: 'root' })
|
|
199
|
+
export class UserService extends BaseService<
|
|
200
|
+
User,
|
|
201
|
+
UserFilter,
|
|
202
|
+
CreateUserDto,
|
|
203
|
+
UpdateUserDto
|
|
204
|
+
> {
|
|
205
|
+
constructor(http: HttpClient) {
|
|
206
|
+
super(http, '/api/users');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### BaseStateService<TRecord, TFilter>
|
|
214
|
+
|
|
215
|
+
Comprehensive reactive state management for data-driven features.
|
|
216
|
+
|
|
217
|
+
#### State Properties
|
|
218
|
+
|
|
219
|
+
| Observable | Type | Description |
|
|
220
|
+
|------------|------|-------------|
|
|
221
|
+
| `filter$` | `Observable<TFilter>` | Current filter state |
|
|
222
|
+
| `records$` | `Observable<TRecord[]>` | Current records |
|
|
223
|
+
| `pager$` | `Observable<IMultiresultMetaData \| null>` | Pagination metadata |
|
|
224
|
+
| `selected$` | `Observable<TRecord \| null>` | Selected record |
|
|
225
|
+
| `loading$` | `Observable<Record<string, boolean>>` | Loading states |
|
|
226
|
+
| `error$` | `Observable<string \| null>` | Error message |
|
|
227
|
+
|
|
228
|
+
#### Getters
|
|
229
|
+
|
|
230
|
+
| Getter | Type | Description |
|
|
231
|
+
|--------|------|-------------|
|
|
232
|
+
| `currentFilter` | `TFilter` | Current filter value |
|
|
233
|
+
| `currentRecords` | `TRecord[]` | Current records value |
|
|
234
|
+
| `selected` | `TRecord \| null` | Selected record value |
|
|
235
|
+
|
|
236
|
+
#### Key Methods
|
|
237
|
+
|
|
238
|
+
**Filter Management**
|
|
239
|
+
- `setFilter(update: Partial<TFilter>)` - Update filter
|
|
240
|
+
- `resetFilter(defaults?)` - Reset to defaults
|
|
241
|
+
- `setPage(page: number)` - Change page
|
|
242
|
+
- `setPageLength(pageLength: number)` - Change page size
|
|
243
|
+
|
|
244
|
+
**Sorting**
|
|
245
|
+
- `setSort(column: string, order?: 'ASC' | 'DESC')` - Add/update sort
|
|
246
|
+
- `removeSort(column: string)` - Remove specific sort
|
|
247
|
+
- `clearSort()` - Clear all sorting
|
|
248
|
+
|
|
249
|
+
**Records Management**
|
|
250
|
+
- `setRecords(records: TRecord[])` - Replace all records
|
|
251
|
+
- `appendRecords(records: TRecord[])` - Add records
|
|
252
|
+
- `removeRecordById(id, idKey?)` - Remove record
|
|
253
|
+
- `replaceRecord(updated: TRecord, idKey?)` - Update record
|
|
254
|
+
|
|
255
|
+
**Pagination**
|
|
256
|
+
- `setPager(pager: IMultiresultMetaData)` - Set pager
|
|
257
|
+
- `setApiResponse(response: IMultiresult<TRecord>)` - Set records + pager
|
|
258
|
+
- `hasMorePages()` - Check if more pages exist
|
|
259
|
+
- `hasPreviousPage()` - Check if previous page exists
|
|
260
|
+
|
|
261
|
+
**Selection**
|
|
262
|
+
- `select(record: TRecord)` - Select record
|
|
263
|
+
- `clearSelection()` - Clear selection
|
|
264
|
+
- `isSelected(record: TRecord, idKey?)` - Check if selected
|
|
265
|
+
|
|
266
|
+
**Loading States**
|
|
267
|
+
- `setLoading(key: string, value: boolean)` - Set loading state
|
|
268
|
+
- `isLoading$(key: string)` - Observable for specific loading key
|
|
269
|
+
- `clearLoading(key?)` - Clear loading state(s)
|
|
270
|
+
|
|
271
|
+
**Error Handling**
|
|
272
|
+
- `setError(error: string | null)` - Set error message
|
|
273
|
+
|
|
274
|
+
**Lifecycle**
|
|
275
|
+
- `reset()` - Reset all state
|
|
276
|
+
- `destroy()` - Reset and complete subscriptions
|
|
277
|
+
- `destroySubscriptions(subjects?)` - Complete specific subjects
|
|
278
|
+
|
|
279
|
+
#### Complete Example
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
283
|
+
import { BaseStateService } from '@mysteryinfosolutions/api-core';
|
|
284
|
+
|
|
285
|
+
interface Product {
|
|
286
|
+
id: number;
|
|
287
|
+
name: string;
|
|
288
|
+
price: number;
|
|
289
|
+
category: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface ProductFilter {
|
|
293
|
+
category?: string;
|
|
294
|
+
minPrice?: number;
|
|
295
|
+
maxPrice?: number;
|
|
296
|
+
page?: number;
|
|
297
|
+
pageLength?: number;
|
|
298
|
+
search?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Component({
|
|
302
|
+
selector: 'app-products',
|
|
303
|
+
template: `
|
|
304
|
+
<div class="filters">
|
|
305
|
+
<input
|
|
306
|
+
[value]="(state.filter$ | async)?.search || ''"
|
|
307
|
+
(input)="onSearch($event)"
|
|
308
|
+
placeholder="Search products...">
|
|
309
|
+
|
|
310
|
+
<select (change)="onCategoryChange($event)">
|
|
311
|
+
<option value="">All Categories</option>
|
|
312
|
+
<option value="electronics">Electronics</option>
|
|
313
|
+
<option value="clothing">Clothing</option>
|
|
314
|
+
</select>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="loading" *ngIf="state.isLoading$('list') | async">
|
|
318
|
+
Loading...
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="error" *ngIf="state.error$ | async as error">
|
|
322
|
+
{{ error }}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<table>
|
|
326
|
+
<thead>
|
|
327
|
+
<tr>
|
|
328
|
+
<th (click)="state.setSort('name')">Name</th>
|
|
329
|
+
<th (click)="state.setSort('price')">Price</th>
|
|
330
|
+
<th>Category</th>
|
|
331
|
+
<th>Actions</th>
|
|
332
|
+
</tr>
|
|
333
|
+
</thead>
|
|
334
|
+
<tbody>
|
|
335
|
+
<tr *ngFor="let product of state.records$ | async"
|
|
336
|
+
[class.selected]="state.isSelected(product)">
|
|
337
|
+
<td>{{ product.name }}</td>
|
|
338
|
+
<td>{{ product.price | currency }}</td>
|
|
339
|
+
<td>{{ product.category }}</td>
|
|
340
|
+
<td>
|
|
341
|
+
<button (click)="state.select(product)">Select</button>
|
|
342
|
+
<button (click)="editProduct(product)">Edit</button>
|
|
343
|
+
<button (click)="deleteProduct(product.id)">Delete</button>
|
|
344
|
+
</td>
|
|
345
|
+
</tr>
|
|
346
|
+
</tbody>
|
|
347
|
+
</table>
|
|
348
|
+
|
|
349
|
+
<div class="pagination" *ngIf="state.pager$ | async as pager">
|
|
350
|
+
<button
|
|
351
|
+
(click)="previousPage()"
|
|
352
|
+
[disabled]="!state.hasPreviousPage()">
|
|
353
|
+
Previous
|
|
354
|
+
</button>
|
|
355
|
+
|
|
356
|
+
<span>
|
|
357
|
+
Page {{ pager.currentPage }} of {{ pager.lastPage }}
|
|
358
|
+
({{ pager.totalRecords }} total)
|
|
359
|
+
</span>
|
|
360
|
+
|
|
361
|
+
<button
|
|
362
|
+
(click)="nextPage()"
|
|
363
|
+
[disabled]="!state.hasMorePages()">
|
|
364
|
+
Next
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
`,
|
|
368
|
+
providers: [BaseStateService]
|
|
369
|
+
})
|
|
370
|
+
export class ProductsComponent implements OnInit, OnDestroy {
|
|
371
|
+
state = new BaseStateService<Product, ProductFilter>();
|
|
372
|
+
|
|
373
|
+
constructor(private productService: ProductService) {}
|
|
374
|
+
|
|
375
|
+
ngOnInit() {
|
|
376
|
+
// Subscribe to filter changes and load data
|
|
377
|
+
this.state.filter$.subscribe(filter => {
|
|
378
|
+
this.loadProducts(filter);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Set initial filter
|
|
382
|
+
this.state.setFilter({
|
|
383
|
+
page: 1,
|
|
384
|
+
pageLength: 25,
|
|
385
|
+
category: 'electronics'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
loadProducts(filter: ProductFilter) {
|
|
390
|
+
this.state.setLoading('list', true);
|
|
391
|
+
this.state.setError(null);
|
|
392
|
+
|
|
393
|
+
this.productService.getAll(filter).subscribe({
|
|
394
|
+
next: (response) => {
|
|
395
|
+
if (response.data) {
|
|
396
|
+
this.state.setApiResponse(response.data);
|
|
397
|
+
}
|
|
398
|
+
this.state.setLoading('list', false);
|
|
399
|
+
},
|
|
400
|
+
error: (err) => {
|
|
401
|
+
this.state.setError('Failed to load products');
|
|
402
|
+
this.state.setLoading('list', false);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
onSearch(event: Event) {
|
|
408
|
+
const search = (event.target as HTMLInputElement).value;
|
|
409
|
+
this.state.setFilter({ search, page: 1 });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
onCategoryChange(event: Event) {
|
|
413
|
+
const category = (event.target as HTMLSelectElement).value;
|
|
414
|
+
this.state.setFilter({ category, page: 1 });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
nextPage() {
|
|
418
|
+
const currentPage = this.state.currentFilter.page || 1;
|
|
419
|
+
this.state.setPage(currentPage + 1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
previousPage() {
|
|
423
|
+
const currentPage = this.state.currentFilter.page || 1;
|
|
424
|
+
this.state.setPage(Math.max(1, currentPage - 1));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
editProduct(product: Product) {
|
|
428
|
+
this.state.select(product);
|
|
429
|
+
// Open edit modal or navigate to edit page
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
deleteProduct(id: number) {
|
|
433
|
+
this.state.setLoading('delete', true);
|
|
434
|
+
|
|
435
|
+
this.productService.delete(id).subscribe({
|
|
436
|
+
next: () => {
|
|
437
|
+
this.state.removeRecordById(id);
|
|
438
|
+
this.state.setLoading('delete', false);
|
|
439
|
+
},
|
|
440
|
+
error: (err) => {
|
|
441
|
+
this.state.setError('Failed to delete product');
|
|
442
|
+
this.state.setLoading('delete', false);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
ngOnDestroy() {
|
|
448
|
+
this.state.destroy();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 🔧 Utilities
|
|
456
|
+
|
|
457
|
+
### Query Builder
|
|
458
|
+
|
|
459
|
+
Convert filter objects to URL query strings.
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { jsonToQueryString, isEmpty } from '@mysteryinfosolutions/api-core';
|
|
463
|
+
|
|
464
|
+
const filter = {
|
|
465
|
+
page: 1,
|
|
466
|
+
pageLength: 25,
|
|
467
|
+
search: 'laptop',
|
|
468
|
+
category: 'electronics',
|
|
469
|
+
tags: ['new', 'sale'],
|
|
470
|
+
sort: [
|
|
471
|
+
{ field: 'price', order: 'ASC' },
|
|
472
|
+
{ field: 'name', order: 'DESC' }
|
|
473
|
+
]
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const queryString = jsonToQueryString(filter);
|
|
477
|
+
// Result: "?page=1&pageLength=25&search=laptop&category=electronics&tags=[new,sale]&sort=price:ASC,name:DESC"
|
|
478
|
+
|
|
479
|
+
// Check if object is empty
|
|
480
|
+
isEmpty({}); // true
|
|
481
|
+
isEmpty({ page: 1 }); // false
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Permission Generator
|
|
485
|
+
|
|
486
|
+
Generate type-safe permission maps for resources.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { generatePermissions } from '@mysteryinfosolutions/api-core';
|
|
490
|
+
|
|
491
|
+
// Standard permissions
|
|
492
|
+
const userPermissions = generatePermissions('user');
|
|
493
|
+
/* Result:
|
|
494
|
+
{
|
|
495
|
+
'*': 'user.*',
|
|
496
|
+
'superAdmin': 'user.superAdmin',
|
|
497
|
+
'view': 'user.view',
|
|
498
|
+
'list': 'user.list',
|
|
499
|
+
'create': 'user.create',
|
|
500
|
+
'update': 'user.update',
|
|
501
|
+
'delete': 'user.delete',
|
|
502
|
+
'restore': 'user.restore',
|
|
503
|
+
'forceDelete': 'user.forceDelete',
|
|
504
|
+
'export': 'user.export',
|
|
505
|
+
'import': 'user.import',
|
|
506
|
+
'approve': 'user.approve',
|
|
507
|
+
'reject': 'user.reject',
|
|
508
|
+
'archive': 'user.archive',
|
|
509
|
+
'unarchive': 'user.unarchive',
|
|
510
|
+
'duplicate': 'user.duplicate',
|
|
511
|
+
'share': 'user.share',
|
|
512
|
+
'assign': 'user.assign',
|
|
513
|
+
'changeStatus': 'user.changeStatus',
|
|
514
|
+
'print': 'user.print',
|
|
515
|
+
'preview': 'user.preview',
|
|
516
|
+
'publish': 'user.publish',
|
|
517
|
+
'unpublish': 'user.unpublish',
|
|
518
|
+
'sync': 'user.sync',
|
|
519
|
+
'audit': 'user.audit',
|
|
520
|
+
'comment': 'user.comment',
|
|
521
|
+
'favorite': 'user.favorite',
|
|
522
|
+
'reorder': 'user.reorder',
|
|
523
|
+
'toggleVisibility': 'user.toggleVisibility',
|
|
524
|
+
'managePermissions': 'user.managePermissions',
|
|
525
|
+
'assignRole': 'user.assignRole',
|
|
526
|
+
'configure': 'user.configure',
|
|
527
|
+
// ... all 30+ standard permissions
|
|
528
|
+
}
|
|
529
|
+
*/
|
|
530
|
+
|
|
531
|
+
// With custom permissions
|
|
532
|
+
const productPermissions = generatePermissions('product', ['discount', 'featured']);
|
|
533
|
+
/* Adds:
|
|
534
|
+
{
|
|
535
|
+
...standardPermissions,
|
|
536
|
+
'discount': 'product.discount',
|
|
537
|
+
'featured': 'product.featured'
|
|
538
|
+
}
|
|
539
|
+
*/
|
|
540
|
+
|
|
541
|
+
// Usage in component
|
|
542
|
+
@Component({
|
|
543
|
+
template: `
|
|
544
|
+
<button *ngIf="hasPermission(permissions.create)">
|
|
545
|
+
Create Product
|
|
546
|
+
</button>
|
|
547
|
+
`
|
|
548
|
+
})
|
|
549
|
+
export class ProductsComponent {
|
|
550
|
+
permissions = generatePermissions('product');
|
|
551
|
+
|
|
552
|
+
hasPermission(permission: string): boolean {
|
|
553
|
+
// Check with your auth service
|
|
554
|
+
return this.authService.hasPermission(permission);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
57
557
|
```
|
|
58
558
|
|
|
59
|
-
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## 🎨 Resource Configuration
|
|
562
|
+
|
|
563
|
+
Standardize table/grid configurations across your app.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { BaseResourceConfig, generatePermissions } from '@mysteryinfosolutions/api-core';
|
|
567
|
+
|
|
568
|
+
interface Product {
|
|
569
|
+
id: number;
|
|
570
|
+
name: string;
|
|
571
|
+
price: number;
|
|
572
|
+
category: string;
|
|
573
|
+
createdAt: string;
|
|
574
|
+
updatedAt: string;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
class ProductConfig extends BaseResourceConfig<Product> {
|
|
578
|
+
columns = [
|
|
579
|
+
{
|
|
580
|
+
key: 'id',
|
|
581
|
+
label: 'ID',
|
|
582
|
+
width: '80px',
|
|
583
|
+
isSortable: true
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
key: 'name',
|
|
587
|
+
label: 'Product Name',
|
|
588
|
+
isSortable: true,
|
|
589
|
+
type: 'text'
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
key: 'price',
|
|
593
|
+
label: 'Price',
|
|
594
|
+
isSortable: true,
|
|
595
|
+
pipe: 'currency',
|
|
596
|
+
width: '120px'
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
key: 'category',
|
|
600
|
+
label: 'Category',
|
|
601
|
+
isSortable: true
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
key: 'updatedAt',
|
|
605
|
+
label: 'Last Updated',
|
|
606
|
+
isSortable: true,
|
|
607
|
+
pipe: 'date',
|
|
608
|
+
hideOnMobile: true
|
|
609
|
+
}
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
searchColumns = ['name', 'category'];
|
|
613
|
+
defaultSortColumn = 'updatedAt';
|
|
614
|
+
defaultSortOrder = 'DESC';
|
|
615
|
+
defaultPageLength = 25;
|
|
616
|
+
pageLengthOptions = [10, 25, 50, 100];
|
|
617
|
+
|
|
618
|
+
modifyModalSize = 'lg';
|
|
619
|
+
summaryModalSize = 'md';
|
|
620
|
+
defaultDetailView = 'summary';
|
|
621
|
+
|
|
622
|
+
permissions = generatePermissions('product');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Usage
|
|
626
|
+
const config = new ProductConfig();
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 📋 Models & Types
|
|
632
|
+
|
|
633
|
+
### IResponse<T>
|
|
634
|
+
|
|
635
|
+
Standard API response wrapper.
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
interface IResponse<T> {
|
|
639
|
+
status?: number;
|
|
640
|
+
data?: T | null;
|
|
641
|
+
error?: IMisError;
|
|
642
|
+
infoDtls?: any;
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### IMultiresult<T>
|
|
647
|
+
|
|
648
|
+
Paginated list response.
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
interface IMultiresult<T> {
|
|
652
|
+
records: T[];
|
|
653
|
+
pager: IMultiresultMetaData;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
interface IMultiresultMetaData {
|
|
657
|
+
totalRecords: number;
|
|
658
|
+
previous?: number;
|
|
659
|
+
currentPage?: number;
|
|
660
|
+
next?: number;
|
|
661
|
+
perPage?: number;
|
|
662
|
+
segment?: number;
|
|
663
|
+
lastPage?: number;
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Filter
|
|
668
|
+
|
|
669
|
+
Base filter class with pagination and sorting.
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
abstract class Filter {
|
|
673
|
+
ids?: number[];
|
|
674
|
+
dateRangeColumn?: string;
|
|
675
|
+
dateRangeFrom?: string;
|
|
676
|
+
dateRangeTo?: string;
|
|
677
|
+
|
|
678
|
+
page?: number;
|
|
679
|
+
pageLength?: number;
|
|
680
|
+
|
|
681
|
+
sort?: SortItem[];
|
|
682
|
+
|
|
683
|
+
search?: string;
|
|
684
|
+
searchColumns?: string;
|
|
685
|
+
selectColumns?: string;
|
|
686
|
+
selectMode?: SELECT_MODE;
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### SortItem
|
|
691
|
+
|
|
692
|
+
Sort configuration.
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
class SortItem {
|
|
696
|
+
constructor(
|
|
697
|
+
public field: string,
|
|
698
|
+
public order: 'ASC' | 'DESC'
|
|
699
|
+
) {}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### TableColumn<T>
|
|
704
|
+
|
|
705
|
+
Column configuration for tables.
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
interface TableColumn<T = any> {
|
|
709
|
+
key: keyof T | string;
|
|
710
|
+
label: string;
|
|
711
|
+
isSortable?: boolean;
|
|
712
|
+
width?: string;
|
|
713
|
+
type?: 'text' | 'checkbox' | 'action' | 'custom';
|
|
714
|
+
valueGetter?: (row: T) => any;
|
|
715
|
+
hideOnMobile?: boolean;
|
|
716
|
+
pipe?: 'date' | 'currency' | 'uppercase' | 'lowercase' | string;
|
|
717
|
+
cellClass?: string;
|
|
718
|
+
headerClass?: string;
|
|
719
|
+
visible?: boolean;
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## 🔌 API Format Requirements
|
|
726
|
+
|
|
727
|
+
This library expects your backend API to follow this format:
|
|
728
|
+
|
|
729
|
+
### Response Format
|
|
730
|
+
|
|
731
|
+
**Success Response:**
|
|
732
|
+
```json
|
|
733
|
+
{
|
|
734
|
+
"status": 200,
|
|
735
|
+
"data": { /* your data */ },
|
|
736
|
+
"infoDtls": null
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**List Response:**
|
|
741
|
+
```json
|
|
742
|
+
{
|
|
743
|
+
"status": 200,
|
|
744
|
+
"data": {
|
|
745
|
+
"records": [
|
|
746
|
+
{ "id": 1, "name": "Item 1" },
|
|
747
|
+
{ "id": 2, "name": "Item 2" }
|
|
748
|
+
],
|
|
749
|
+
"pager": {
|
|
750
|
+
"totalRecords": 100,
|
|
751
|
+
"currentPage": 1,
|
|
752
|
+
"lastPage": 10,
|
|
753
|
+
"perPage": 10,
|
|
754
|
+
"next": 2,
|
|
755
|
+
"previous": null
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Error Response:**
|
|
762
|
+
```json
|
|
763
|
+
{
|
|
764
|
+
"status": 400,
|
|
765
|
+
"data": null,
|
|
766
|
+
"error": {
|
|
767
|
+
"code": "VALIDATION_ERROR",
|
|
768
|
+
"message": "Invalid input",
|
|
769
|
+
"details": "Email is required"
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Query String Format
|
|
775
|
+
|
|
776
|
+
**Pagination:**
|
|
777
|
+
```
|
|
778
|
+
?page=1&pageLength=25
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
**Sorting:**
|
|
782
|
+
```
|
|
783
|
+
?sort=name:ASC,createdAt:DESC
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Filtering:**
|
|
787
|
+
```
|
|
788
|
+
?search=laptop&category=electronics&minPrice=100
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Combined:**
|
|
792
|
+
```
|
|
793
|
+
?page=1&pageLength=25&sort=price:ASC&search=laptop&category=electronics
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## 🏗 Best Practices
|
|
799
|
+
|
|
800
|
+
### 1. Service Pattern
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// ✅ Good: Extend BaseService
|
|
804
|
+
@Injectable({ providedIn: 'root' })
|
|
805
|
+
export class UserService extends BaseService<User, UserFilter> {
|
|
806
|
+
constructor(http: HttpClient) {
|
|
807
|
+
super(http, '/api/users');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Add custom methods
|
|
811
|
+
activateUser(id: number): Observable<IResponse<User>> {
|
|
812
|
+
return this.http.post<IResponse<User>>(`${this.baseUrl}/${id}/activate`, {});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ❌ Bad: Don't reimplement CRUD
|
|
817
|
+
@Injectable({ providedIn: 'root' })
|
|
818
|
+
export class UserService {
|
|
819
|
+
getAll() { /* duplicate code */ }
|
|
820
|
+
getDetails() { /* duplicate code */ }
|
|
821
|
+
// ...
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### 2. State Management
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
// ✅ Good: Use BaseStateService for complex lists
|
|
829
|
+
@Component({
|
|
830
|
+
providers: [BaseStateService] // Component-level
|
|
831
|
+
})
|
|
832
|
+
export class UsersComponent {
|
|
833
|
+
state = new BaseStateService<User, UserFilter>();
|
|
834
|
+
|
|
835
|
+
ngOnDestroy() {
|
|
836
|
+
this.state.destroy(); // Clean up
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ✅ Good: Direct observable for simple cases
|
|
841
|
+
@Component({})
|
|
842
|
+
export class UserDetailComponent {
|
|
843
|
+
user$ = this.userService.getDetails(this.userId);
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### 3. Error Handling
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// ✅ Good: Handle errors properly
|
|
851
|
+
loadUsers() {
|
|
852
|
+
this.state.setLoading('list', true);
|
|
853
|
+
this.state.setError(null);
|
|
854
|
+
|
|
855
|
+
this.userService.getAll(this.state.currentFilter).subscribe({
|
|
856
|
+
next: (response) => {
|
|
857
|
+
if (response.data) {
|
|
858
|
+
this.state.setApiResponse(response.data);
|
|
859
|
+
} else if (response.error) {
|
|
860
|
+
this.state.setError(response.error.message || 'Failed to load');
|
|
861
|
+
}
|
|
862
|
+
this.state.setLoading('list', false);
|
|
863
|
+
},
|
|
864
|
+
error: (err) => {
|
|
865
|
+
this.state.setError('Network error occurred');
|
|
866
|
+
this.state.setLoading('list', false);
|
|
867
|
+
console.error('Error loading users:', err);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### 4. Filter Management
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
// ✅ Good: Reset page when filter changes
|
|
877
|
+
onCategoryChange(category: string) {
|
|
878
|
+
this.state.setFilter({
|
|
879
|
+
category,
|
|
880
|
+
page: 1 // Reset to first page
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ✅ Good: Debounce search input
|
|
885
|
+
searchInput$ = new Subject<string>();
|
|
886
|
+
|
|
887
|
+
ngOnInit() {
|
|
888
|
+
this.searchInput$.pipe(
|
|
889
|
+
debounceTime(300),
|
|
890
|
+
distinctUntilChanged()
|
|
891
|
+
).subscribe(search => {
|
|
892
|
+
this.state.setFilter({ search, page: 1 });
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### 5. Permissions
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
// ✅ Good: Generate once, reuse everywhere
|
|
901
|
+
export class UserConfig extends BaseResourceConfig<User> {
|
|
902
|
+
permissions = generatePermissions('user', ['resetPassword', 'sendInvite']);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// In component
|
|
906
|
+
if (this.authService.hasPermission(config.permissions.create)) {
|
|
907
|
+
// Show create button
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## 🔄 Migration from v1.x to v2.x
|
|
914
|
+
|
|
915
|
+
When updating to future versions, check the CHANGELOG.md for breaking changes.
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## 🤝 Contributing
|
|
920
|
+
|
|
921
|
+
Contributions are welcome! Please follow these guidelines:
|
|
922
|
+
|
|
923
|
+
1. Fork the repository
|
|
924
|
+
2. Create a feature branch
|
|
925
|
+
3. Write tests for new features
|
|
926
|
+
4. Ensure all tests pass
|
|
927
|
+
5. Submit a pull request
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
## 📄 License
|
|
932
|
+
|
|
933
|
+
MIT License - see LICENSE file for details
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## 🐛 Issues & Support
|
|
938
|
+
|
|
939
|
+
For issues, questions, or feature requests:
|
|
940
|
+
- GitHub Issues: [Create an issue](https://github.com/mysteryinfosolutions/angular-libraries/issues)
|
|
941
|
+
- Email: support@mysteryinfosolutions.com
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## 🙏 Credits
|
|
946
|
+
|
|
947
|
+
Developed and maintained by **Mystery Info Solutions**.
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## 📚 Additional Resources
|
|
952
|
+
|
|
953
|
+
- [Angular Documentation](https://angular.dev)
|
|
954
|
+
- [RxJS Documentation](https://rxjs.dev)
|
|
955
|
+
- [TypeScript Documentation](https://www.typescriptlang.org)
|
|
60
956
|
|
|
61
|
-
|
|
957
|
+
---
|
|
62
958
|
|
|
63
|
-
|
|
959
|
+
**Version:** 1.8.0
|
|
960
|
+
**Last Updated:** November 2024
|