@flusys/nestjs-shared 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +851 -275
- package/cjs/classes/api-controller.class.js +4 -4
- package/cjs/decorators/require-permission.decorator.js +7 -3
- package/cjs/exceptions/permission.exception.js +2 -2
- package/cjs/guards/permission.guard.js +66 -65
- package/cjs/interfaces/permission.interface.js +1 -1
- package/cjs/utils/html-sanitizer.util.js +9 -9
- package/cjs/utils/request.util.js +2 -1
- package/classes/api-controller.class.d.ts +3 -3
- package/decorators/require-permission.decorator.d.ts +3 -2
- package/exceptions/permission.exception.d.ts +1 -1
- package/fesm/classes/api-controller.class.js +5 -5
- package/fesm/decorators/require-permission.decorator.js +18 -65
- package/fesm/exceptions/permission.exception.js +2 -2
- package/fesm/guards/permission.guard.js +66 -65
- package/fesm/interfaces/permission.interface.js +1 -3
- package/fesm/utils/request.util.js +2 -1
- package/guards/permission.guard.d.ts +2 -4
- package/interfaces/permission.interface.d.ts +13 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Shared Package Guide
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flusys/nestjs-shared`
|
|
4
|
+
> **Version:** 3.0.0
|
|
4
5
|
> **Type:** Shared NestJS utilities, classes, decorators, guards, and modules
|
|
5
6
|
|
|
6
7
|
This comprehensive guide covers the shared package - the shared NestJS infrastructure layer.
|
|
@@ -23,6 +24,7 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
23
24
|
- [Utilities](#utilities)
|
|
24
25
|
- [Error Handling](#error-handling)
|
|
25
26
|
- [Constants](#constants)
|
|
27
|
+
- [Logger System](#logger-system)
|
|
26
28
|
- [API Reference](#api-reference)
|
|
27
29
|
|
|
28
30
|
---
|
|
@@ -32,13 +34,14 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
32
34
|
`@flusys/nestjs-shared` provides shared utilities for building scalable NestJS applications:
|
|
33
35
|
|
|
34
36
|
- **Generic CRUD** - Standardized API controller and service patterns
|
|
35
|
-
- **Permission System** - Role and permission-based access control with complex logic
|
|
37
|
+
- **Permission System** - Role and permission-based access control with complex logic and wildcard matching
|
|
36
38
|
- **Caching** - In-memory + Redis hybrid caching (HybridCache)
|
|
37
|
-
- **Request Correlation** - AsyncLocalStorage-based request tracking
|
|
39
|
+
- **Request Correlation** - AsyncLocalStorage-based request tracking with correlation IDs
|
|
38
40
|
- **Middleware** - Logging, correlation, and performance monitoring
|
|
39
41
|
- **Interceptors** - Response metadata, idempotency, auto field setting
|
|
40
|
-
- **Multi-Tenancy** - Dynamic database connection management
|
|
42
|
+
- **Multi-Tenancy** - Dynamic database connection management with connection pooling
|
|
41
43
|
- **Error Handling** - Centralized error handling with sensitive data redaction
|
|
44
|
+
- **Logging** - Winston-based logging with tenant-aware routing and daily rotation
|
|
42
45
|
|
|
43
46
|
### Package Hierarchy
|
|
44
47
|
|
|
@@ -63,35 +66,34 @@ nestjs-shared/
|
|
|
63
66
|
├── src/
|
|
64
67
|
│ ├── classes/ # Base classes
|
|
65
68
|
│ │ ├── api-service.class.ts # Generic CRUD service
|
|
66
|
-
│ │ ├── api-controller.class.ts # Generic CRUD controller factory
|
|
69
|
+
│ │ ├── api-controller.class.ts # Generic CRUD controller factory (createApiController)
|
|
67
70
|
│ │ ├── request-scoped-api.service.ts # REQUEST-scoped service base
|
|
68
|
-
│ │ ├── hybrid-cache.class.ts # Two-tier caching
|
|
69
|
-
│ │ ├── winston.logger.class.ts # Winston logger config
|
|
70
|
-
│ │
|
|
71
|
-
│ │ └── nest-logger-adapter.class.ts
|
|
71
|
+
│ │ ├── hybrid-cache.class.ts # Two-tier caching (memory + Redis)
|
|
72
|
+
│ │ ├── winston.logger.class.ts # Winston logger config with tenant routing
|
|
73
|
+
│ │ └── winston-logger-adapter.class.ts # ILogger adapters
|
|
72
74
|
│ │
|
|
73
75
|
│ ├── constants/ # Injection tokens & constants
|
|
74
|
-
│ │ ├── permissions.ts # Permission constants
|
|
75
|
-
│ │ └── index.ts
|
|
76
|
+
│ │ ├── permissions.ts # Permission constants (PERMISSIONS)
|
|
77
|
+
│ │ └── index.ts # Metadata keys, injection tokens, headers
|
|
76
78
|
│ │
|
|
77
79
|
│ ├── decorators/ # Custom decorators
|
|
78
80
|
│ │ ├── api-response.decorator.ts # @ApiResponseDto
|
|
79
81
|
│ │ ├── current-user.decorator.ts # @CurrentUser
|
|
80
82
|
│ │ ├── public.decorator.ts # @Public
|
|
81
|
-
│ │ ├── require-permission.decorator.ts # @RequirePermission
|
|
82
|
-
│ │ ├── sanitize.decorator.ts
|
|
83
|
+
│ │ ├── require-permission.decorator.ts # @RequirePermission, @RequireAnyPermission, @RequirePermissionLogic
|
|
84
|
+
│ │ ├── sanitize-html.decorator.ts # @SanitizeHtml, @SanitizeAndTrim
|
|
83
85
|
│ │ └── index.ts
|
|
84
86
|
│ │
|
|
85
87
|
│ ├── dtos/ # Shared DTOs
|
|
86
|
-
│ │ ├── delete.dto.ts
|
|
87
|
-
│ │ ├── filter-and-pagination.dto.ts
|
|
88
|
-
│ │ ├── identity-response.dto.ts
|
|
89
|
-
│ │ ├── pagination.dto.ts
|
|
90
|
-
│ │ ├── response-payload.dto.ts
|
|
88
|
+
│ │ ├── delete.dto.ts # DeleteDto with soft/restore/permanent
|
|
89
|
+
│ │ ├── filter-and-pagination.dto.ts # FilterAndPaginationDto, GetByIdBodyDto
|
|
90
|
+
│ │ ├── identity-response.dto.ts # IdentityResponseDto
|
|
91
|
+
│ │ ├── pagination.dto.ts # PaginationDto
|
|
92
|
+
│ │ ├── response-payload.dto.ts # Single/List/Bulk/Message response DTOs
|
|
91
93
|
│ │ └── index.ts
|
|
92
94
|
│ │
|
|
93
95
|
│ ├── entities/ # Base entities
|
|
94
|
-
│ │ ├── identity.ts # Base entity with UUID
|
|
96
|
+
│ │ ├── identity.ts # Base entity with UUID & audit fields
|
|
95
97
|
│ │ ├── user-root.ts # Base user entity
|
|
96
98
|
│ │ └── index.ts
|
|
97
99
|
│ │
|
|
@@ -99,45 +101,44 @@ nestjs-shared/
|
|
|
99
101
|
│ │ └── permission.exception.ts # Permission-related exceptions
|
|
100
102
|
│ │
|
|
101
103
|
│ ├── guards/ # Authentication & authorization
|
|
102
|
-
│ │ ├── jwt-auth.guard.ts # JWT token validation
|
|
103
|
-
│ │ └── permission.guard.ts # Permission checks
|
|
104
|
+
│ │ ├── jwt-auth.guard.ts # JWT token validation with @Public support
|
|
105
|
+
│ │ └── permission.guard.ts # Permission checks with wildcard matching
|
|
104
106
|
│ │
|
|
105
107
|
│ ├── interceptors/ # Request/response interceptors
|
|
106
108
|
│ │ ├── delete-empty-id-from-body.interceptor.ts
|
|
107
|
-
│ │ ├── idempotency.interceptor.ts
|
|
109
|
+
│ │ ├── idempotency.interceptor.ts # Prevents duplicate POST requests
|
|
108
110
|
│ │ ├── query-performance.interceptor.ts
|
|
109
|
-
│ │ ├── response-meta.interceptor.ts
|
|
110
|
-
│ │ ├── set-
|
|
111
|
-
│ │
|
|
112
|
-
│ │ ├── set-update-by-on-body.interceptor.ts
|
|
113
|
-
│ │ └── slug.interceptor.ts
|
|
111
|
+
│ │ ├── response-meta.interceptor.ts # Adds _meta to responses
|
|
112
|
+
│ │ ├── set-user-field-on-body.interceptor.ts # Factory + SetCreatedByOnBody, etc.
|
|
113
|
+
│ │ └── slug.interceptor.ts # Auto-generates slug from name
|
|
114
114
|
│ │
|
|
115
115
|
│ ├── interfaces/ # TypeScript interfaces
|
|
116
116
|
│ │ ├── api.interface.ts # IService interface
|
|
117
|
-
│ │ ├──
|
|
117
|
+
│ │ ├── datasource.interface.ts # IDataSourceProvider
|
|
118
|
+
│ │ ├── identity.interface.ts # IIdentity interface
|
|
118
119
|
│ │ ├── logged-user-info.interface.ts # ILoggedUserInfo
|
|
119
|
-
│ │ ├── logger.interface.ts # ILogger
|
|
120
|
-
│ │ ├──
|
|
121
|
-
│ │ └──
|
|
120
|
+
│ │ ├── logger.interface.ts # ILogger interface
|
|
121
|
+
│ │ ├── module-config.interface.ts # IModuleConfigService
|
|
122
|
+
│ │ └── permission.interface.ts # ILogicNode, PermissionConfig, PermissionGuardConfig
|
|
122
123
|
│ │
|
|
123
124
|
│ ├── middlewares/ # Middleware
|
|
124
|
-
│ │ └── logger.middleware.ts # Request
|
|
125
|
+
│ │ └── logger.middleware.ts # Request correlation with AsyncLocalStorage
|
|
125
126
|
│ │
|
|
126
127
|
│ ├── modules/ # NestJS modules
|
|
127
|
-
│ │ ├── cache/cache.module.ts
|
|
128
|
+
│ │ ├── cache/cache.module.ts # CacheModule.forRoot()
|
|
128
129
|
│ │ ├── datasource/
|
|
129
|
-
│ │ │ ├── datasource.module.ts
|
|
130
|
+
│ │ │ ├── datasource.module.ts # DataSourceModule.forRoot/forRootAsync/forFeature
|
|
130
131
|
│ │ │ └── multi-tenant-datasource.service.ts
|
|
131
132
|
│ │ └── utils/
|
|
132
|
-
│ │ ├── utils.module.ts
|
|
133
|
-
│ │ └── utils.service.ts
|
|
133
|
+
│ │ ├── utils.module.ts # Global UtilsModule
|
|
134
|
+
│ │ └── utils.service.ts # Cache helpers, string utilities
|
|
134
135
|
│ │
|
|
135
136
|
│ └── utils/ # Utility functions
|
|
136
|
-
│ ├── error-handler.util.ts
|
|
137
|
-
│ ├──
|
|
138
|
-
│ ├──
|
|
139
|
-
│ ├── request.util.ts
|
|
140
|
-
│ └──
|
|
137
|
+
│ ├── error-handler.util.ts # ErrorHandler class
|
|
138
|
+
│ ├── html-sanitizer.util.ts # escapeHtml, escapeHtmlVariables
|
|
139
|
+
│ ├── query-helpers.util.ts # applyCompanyFilter, validateCompanyOwnership
|
|
140
|
+
│ ├── request.util.ts # isBrowserRequest, buildCookieOptions, parseDurationToMs
|
|
141
|
+
│ └── string.util.ts # generateSlug, generateUniqueSlug
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
---
|
|
@@ -149,10 +150,12 @@ The `ApiService` base class provides standardized CRUD operations with caching,
|
|
|
149
150
|
### Basic Usage
|
|
150
151
|
|
|
151
152
|
```typescript
|
|
152
|
-
import { ApiService, HybridCache } from '@flusys/nestjs-shared
|
|
153
|
-
import { UtilsService } from '@flusys/nestjs-shared
|
|
153
|
+
import { ApiService, HybridCache } from '@flusys/nestjs-shared';
|
|
154
|
+
import { UtilsService } from '@flusys/nestjs-shared';
|
|
154
155
|
import { Injectable, Inject } from '@nestjs/common';
|
|
156
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
155
157
|
import { Repository } from 'typeorm';
|
|
158
|
+
import { CACHE_INSTANCE } from '@flusys/nestjs-shared';
|
|
156
159
|
|
|
157
160
|
@Injectable()
|
|
158
161
|
export class UserService extends ApiService<
|
|
@@ -165,17 +168,17 @@ export class UserService extends ApiService<
|
|
|
165
168
|
constructor(
|
|
166
169
|
@InjectRepository(User)
|
|
167
170
|
protected override repository: Repository<User>,
|
|
168
|
-
@Inject(
|
|
171
|
+
@Inject(CACHE_INSTANCE)
|
|
169
172
|
protected override cacheManager: HybridCache,
|
|
170
173
|
@Inject(UtilsService)
|
|
171
174
|
protected override utilsService: UtilsService,
|
|
172
175
|
) {
|
|
173
176
|
super(
|
|
174
|
-
'user', // Entity name (for query
|
|
177
|
+
'user', // Entity name (for query alias)
|
|
175
178
|
repository,
|
|
176
179
|
cacheManager,
|
|
177
180
|
utilsService,
|
|
178
|
-
'UserService', //
|
|
181
|
+
'UserService', // Logger context name
|
|
179
182
|
true, // Enable caching
|
|
180
183
|
);
|
|
181
184
|
}
|
|
@@ -184,71 +187,94 @@ export class UserService extends ApiService<
|
|
|
184
187
|
|
|
185
188
|
### ApiService Methods
|
|
186
189
|
|
|
187
|
-
| Method | Description |
|
|
188
|
-
|
|
189
|
-
| `insert(dto, user)
|
|
190
|
-
| `insertMany(dtos, user)
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
190
|
+
| Method | Signature | Description |
|
|
191
|
+
|--------|-----------|-------------|
|
|
192
|
+
| `insert` | `(dto, user) → Promise<T>` | Create single entity |
|
|
193
|
+
| `insertMany` | `(dtos[], user) → Promise<T[]>` | Create multiple entities |
|
|
194
|
+
| `findById` | `(id, user, select?) → Promise<T>` | Find entity by ID (throws if not found) |
|
|
195
|
+
| `findByIds` | `(ids[], user) → Promise<T[]>` | Find multiple by IDs |
|
|
196
|
+
| `getAll` | `(search, filterDto, user) → Promise<{data, total}>` | Get paginated list with filters |
|
|
197
|
+
| `update` | `(dto, user) → Promise<T>` | Update single entity |
|
|
198
|
+
| `updateMany` | `(dtos[], user) → Promise<T[]>` | Update multiple entities |
|
|
199
|
+
| `delete` | `(deleteDto, user) → Promise<null>` | Soft/permanent delete or restore |
|
|
200
|
+
| `clearCacheForAll` | `() → Promise<void>` | Clear all entity cache |
|
|
201
|
+
| `clearCacheForId` | `(entities[]) → Promise<void>` | Clear cache for specific entities |
|
|
198
202
|
|
|
199
203
|
### Customization Hooks
|
|
200
204
|
|
|
201
205
|
```typescript
|
|
202
206
|
@Injectable()
|
|
203
207
|
export class UserService extends ApiService<...> {
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
208
|
+
// Repository initialization (for RequestScopedApiService)
|
|
209
|
+
protected async ensureRepositoryInitialized(): Promise<void> { }
|
|
210
|
+
|
|
211
|
+
// Convert DTO to entity (called for single item)
|
|
212
|
+
protected async convertSingleDtoToEntity(
|
|
213
|
+
dto: CreateDtoT | UpdateDtoT,
|
|
214
|
+
user: ILoggedUserInfo | null
|
|
215
|
+
): Promise<EntityT> { }
|
|
216
|
+
|
|
217
|
+
// Convert entity to response DTO
|
|
218
|
+
protected convertEntityToResponseDto(entity: EntityT, isRaw: boolean): InterfaceT { }
|
|
219
|
+
|
|
220
|
+
// Convert entity list to response list
|
|
221
|
+
protected convertEntityListToResponseListDto(entities: EntityT[], isRaw: boolean): InterfaceT[] { }
|
|
222
|
+
|
|
223
|
+
// Customize SELECT query (add joins, selections)
|
|
224
|
+
protected async getSelectQuery(
|
|
225
|
+
query: SelectQueryBuilder<EntityT>,
|
|
226
|
+
user: ILoggedUserInfo | null,
|
|
227
|
+
select?: string[]
|
|
228
|
+
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
229
|
+
|
|
230
|
+
// Add WHERE filters from filter object
|
|
231
|
+
protected async getFilterQuery(
|
|
232
|
+
query: SelectQueryBuilder<EntityT>,
|
|
233
|
+
filter: Record<string, any>,
|
|
234
|
+
user: ILoggedUserInfo | null
|
|
235
|
+
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
236
|
+
|
|
237
|
+
// Add global search conditions (default: searches 'name' field)
|
|
238
|
+
protected async getGlobalSearchQuery(
|
|
239
|
+
query: SelectQueryBuilder<EntityT>,
|
|
240
|
+
search: string,
|
|
241
|
+
user: ILoggedUserInfo | null
|
|
242
|
+
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
243
|
+
|
|
244
|
+
// Add sort conditions
|
|
245
|
+
protected async getSortQuery(
|
|
246
|
+
query: SelectQueryBuilder<EntityT>,
|
|
247
|
+
sort: Record<string, 'ASC' | 'DESC'>,
|
|
248
|
+
user: ILoggedUserInfo | null
|
|
249
|
+
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
218
250
|
|
|
219
251
|
// Add extra query conditions (e.g., company filtering)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// Before delete hook
|
|
235
|
-
override async beforeDeleteOperation(dto, user, queryRunner): Promise<void> { }
|
|
236
|
-
|
|
237
|
-
// After delete hook
|
|
238
|
-
override async afterDeleteOperation(dto, user, queryRunner): Promise<void> { }
|
|
252
|
+
protected async getExtraManipulateQuery(
|
|
253
|
+
query: SelectQueryBuilder<EntityT>,
|
|
254
|
+
dto: FilterAndPaginationDto,
|
|
255
|
+
user: ILoggedUserInfo | null
|
|
256
|
+
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
257
|
+
|
|
258
|
+
// Lifecycle hooks
|
|
259
|
+
protected async beforeInsertOperation(dto, user, queryRunner): Promise<void> { }
|
|
260
|
+
protected async afterInsertOperation(entities[], user, queryRunner): Promise<void> { }
|
|
261
|
+
protected async beforeUpdateOperation(dto, user, queryRunner): Promise<void> { }
|
|
262
|
+
protected async afterUpdateOperation(entities[], user, queryRunner): Promise<void> { }
|
|
263
|
+
protected async beforeDeleteOperation(dto, user, queryRunner): Promise<void> { }
|
|
264
|
+
protected async afterDeleteOperation(entities[], user, queryRunner): Promise<void> { }
|
|
239
265
|
}
|
|
240
|
-
```
|
|
241
266
|
|
|
242
267
|
---
|
|
243
268
|
|
|
244
269
|
## RequestScopedApiService
|
|
245
270
|
|
|
246
|
-
For dynamic entity resolution based on runtime configuration (e.g., company feature).
|
|
271
|
+
For dynamic entity resolution based on runtime configuration (e.g., company feature toggling). Extends `ApiService` with lazy repository initialization.
|
|
247
272
|
|
|
248
273
|
```typescript
|
|
249
|
-
import { RequestScopedApiService } from '@flusys/nestjs-shared
|
|
274
|
+
import { RequestScopedApiService, HybridCache, CACHE_INSTANCE } from '@flusys/nestjs-shared';
|
|
275
|
+
import { UtilsService, IDataSourceProvider } from '@flusys/nestjs-shared';
|
|
250
276
|
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
251
|
-
import { EntityTarget, Repository } from 'typeorm';
|
|
277
|
+
import { DataSource, EntityTarget, Repository } from 'typeorm';
|
|
252
278
|
|
|
253
279
|
@Injectable({ scope: Scope.REQUEST })
|
|
254
280
|
export class RoleService extends RequestScopedApiService<
|
|
@@ -258,8 +284,10 @@ export class RoleService extends RequestScopedApiService<
|
|
|
258
284
|
RoleBase,
|
|
259
285
|
Repository<RoleBase>
|
|
260
286
|
> {
|
|
287
|
+
private actionRepository!: Repository<Action>;
|
|
288
|
+
|
|
261
289
|
constructor(
|
|
262
|
-
@Inject(
|
|
290
|
+
@Inject(CACHE_INSTANCE) protected override cacheManager: HybridCache,
|
|
263
291
|
@Inject(UtilsService) protected override utilsService: UtilsService,
|
|
264
292
|
@Inject(ModuleConfigService) private readonly config: ModuleConfigService,
|
|
265
293
|
@Inject(DataSourceProvider) private readonly provider: DataSourceProvider,
|
|
@@ -268,42 +296,55 @@ export class RoleService extends RequestScopedApiService<
|
|
|
268
296
|
super('role', null as any, cacheManager, utilsService, 'RoleService', true);
|
|
269
297
|
}
|
|
270
298
|
|
|
271
|
-
// Required: Resolve which entity to use
|
|
299
|
+
// Required: Resolve which entity class to use based on runtime config
|
|
272
300
|
protected resolveEntity(): EntityTarget<RoleBase> {
|
|
273
301
|
return this.config.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
274
302
|
}
|
|
275
303
|
|
|
276
|
-
// Required: Return the DataSource provider
|
|
304
|
+
// Required: Return the DataSource provider for repository creation
|
|
277
305
|
protected getDataSourceProvider(): IDataSourceProvider {
|
|
278
306
|
return this.provider;
|
|
279
307
|
}
|
|
280
308
|
|
|
281
|
-
//
|
|
282
|
-
protected async
|
|
283
|
-
|
|
309
|
+
// Override to initialize additional repositories alongside primary
|
|
310
|
+
protected override async ensureRepositoryInitialized(): Promise<void> {
|
|
311
|
+
await super.ensureRepositoryInitialized();
|
|
312
|
+
// Initialize additional repositories if needed
|
|
313
|
+
const repos = await this.initializeAdditionalRepositories([Action]);
|
|
314
|
+
this.actionRepository = repos[0];
|
|
284
315
|
}
|
|
285
316
|
}
|
|
286
317
|
```
|
|
287
318
|
|
|
288
319
|
### Key Methods
|
|
289
320
|
|
|
290
|
-
| Method | Description |
|
|
291
|
-
|
|
292
|
-
| `ensureRepositoryInitialized()` |
|
|
293
|
-
| `resolveEntity()` | Abstract -
|
|
294
|
-
| `getDataSourceProvider()` | Abstract -
|
|
295
|
-
| `
|
|
321
|
+
| Method | Signature | Description |
|
|
322
|
+
|--------|-----------|-------------|
|
|
323
|
+
| `ensureRepositoryInitialized()` | `() → Promise<void>` | Auto-called before operations, initializes repository |
|
|
324
|
+
| `resolveEntity()` | `() → EntityTarget<EntityT>` | **Abstract** - Return entity class based on runtime config |
|
|
325
|
+
| `getDataSourceProvider()` | `() → IDataSourceProvider` | **Abstract** - Return DataSource provider |
|
|
326
|
+
| `initializeAdditionalRepositories()` | `(entities[]) → Promise<Repository[]>` | Initialize extra repositories |
|
|
327
|
+
| `getDataSourceForService()` | `() → Promise<DataSource>` | Get raw DataSource for transactions |
|
|
328
|
+
|
|
329
|
+
### IDataSourceProvider Interface
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
interface IDataSourceProvider {
|
|
333
|
+
getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>>;
|
|
334
|
+
getDataSource(): Promise<DataSource>;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
296
337
|
|
|
297
338
|
---
|
|
298
339
|
|
|
299
340
|
## ApiController - Generic CRUD Controller
|
|
300
341
|
|
|
301
|
-
The `createApiController` factory creates standardized POST-only RPC controllers.
|
|
342
|
+
The `createApiController` factory creates standardized POST-only RPC controllers with full Swagger documentation.
|
|
302
343
|
|
|
303
344
|
### Basic Usage
|
|
304
345
|
|
|
305
346
|
```typescript
|
|
306
|
-
import { createApiController } from '@flusys/nestjs-shared
|
|
347
|
+
import { createApiController } from '@flusys/nestjs-shared';
|
|
307
348
|
import { Controller, Inject } from '@nestjs/common';
|
|
308
349
|
import { ApiTags } from '@nestjs/swagger';
|
|
309
350
|
|
|
@@ -324,26 +365,51 @@ export class UserController extends createApiController<
|
|
|
324
365
|
|
|
325
366
|
### Generated Endpoints
|
|
326
367
|
|
|
327
|
-
| Endpoint | Method | Description |
|
|
328
|
-
|
|
329
|
-
| `/insert` | POST | Create entity |
|
|
330
|
-
| `/insert-many` | POST | Create multiple entities |
|
|
331
|
-
| `/get/:id` | POST | Get entity by ID |
|
|
332
|
-
| `/get-all` | POST | Get paginated list |
|
|
333
|
-
| `/update` | POST | Update entity |
|
|
334
|
-
| `/update-many` | POST | Update multiple entities |
|
|
335
|
-
| `/delete` | POST | Delete
|
|
368
|
+
| Endpoint | Method | Description | Interceptors |
|
|
369
|
+
|----------|--------|-------------|--------------|
|
|
370
|
+
| `/insert` | POST | Create entity | Idempotency, SetCreatedByOnBody, Slug |
|
|
371
|
+
| `/insert-many` | POST | Create multiple entities | Idempotency, SetCreatedByOnBody, Slug |
|
|
372
|
+
| `/get/:id` | POST | Get entity by ID | - |
|
|
373
|
+
| `/get-all` | POST | Get paginated list with filters | - |
|
|
374
|
+
| `/update` | POST | Update entity | SetUpdateByOnBody, Slug |
|
|
375
|
+
| `/update-many` | POST | Update multiple entities | SetUpdateByOnBody, Slug |
|
|
376
|
+
| `/delete` | POST | Delete/restore/permanent delete | SetDeletedByOnBody |
|
|
377
|
+
|
|
378
|
+
### Security Configuration Types
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Security levels
|
|
382
|
+
type SecurityLevel = 'public' | 'jwt' | 'permission';
|
|
383
|
+
|
|
384
|
+
// Endpoint names
|
|
385
|
+
type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
|
|
386
|
+
|
|
387
|
+
// Endpoint security config
|
|
388
|
+
interface EndpointSecurity {
|
|
389
|
+
level: SecurityLevel;
|
|
390
|
+
permissions?: string[]; // Required permissions
|
|
391
|
+
operator?: 'AND' | 'OR'; // How to combine permissions (default: AND)
|
|
392
|
+
logic?: IPermissionLogic; // Complex permission logic tree
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Per-endpoint or global config
|
|
396
|
+
type ApiSecurityConfig = { [K in ApiEndpoint]?: EndpointSecurity | SecurityLevel };
|
|
397
|
+
|
|
398
|
+
interface ApiControllerOptions {
|
|
399
|
+
security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
|
|
400
|
+
}
|
|
401
|
+
```
|
|
336
402
|
|
|
337
|
-
### Security Configuration
|
|
403
|
+
### Security Configuration Examples
|
|
338
404
|
|
|
339
405
|
```typescript
|
|
340
|
-
// Global security
|
|
406
|
+
// Global security - all endpoints require JWT
|
|
341
407
|
@Controller('users')
|
|
342
408
|
export class UserController extends createApiController(
|
|
343
409
|
CreateUserDto,
|
|
344
410
|
UpdateUserDto,
|
|
345
411
|
UserResponseDto,
|
|
346
|
-
{ security: 'jwt' },
|
|
412
|
+
{ security: 'jwt' },
|
|
347
413
|
) {}
|
|
348
414
|
|
|
349
415
|
// Per-endpoint security
|
|
@@ -356,14 +422,43 @@ export class UserController extends createApiController(
|
|
|
356
422
|
security: {
|
|
357
423
|
getAll: 'public', // No auth required
|
|
358
424
|
getById: 'jwt', // JWT required
|
|
359
|
-
insert: { level: 'permission', permissions: ['
|
|
360
|
-
update: { level: 'permission', permissions: ['
|
|
361
|
-
delete: { level: 'permission', permissions: ['
|
|
425
|
+
insert: { level: 'permission', permissions: ['user.create'] },
|
|
426
|
+
update: { level: 'permission', permissions: ['user.update'] },
|
|
427
|
+
delete: { level: 'permission', permissions: ['user.delete'] },
|
|
362
428
|
},
|
|
363
429
|
},
|
|
364
430
|
) {}
|
|
431
|
+
|
|
432
|
+
// Permission with OR logic
|
|
433
|
+
{
|
|
434
|
+
security: {
|
|
435
|
+
getAll: { level: 'permission', permissions: ['user.read', 'admin'], operator: 'OR' },
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Complex permission logic
|
|
440
|
+
{
|
|
441
|
+
security: {
|
|
442
|
+
delete: {
|
|
443
|
+
level: 'permission',
|
|
444
|
+
logic: {
|
|
445
|
+
type: 'group',
|
|
446
|
+
operator: 'AND',
|
|
447
|
+
children: [
|
|
448
|
+
{ type: 'action', actionId: 'user.delete' },
|
|
449
|
+
{ type: 'group', operator: 'OR', children: [
|
|
450
|
+
{ type: 'action', actionId: 'admin' },
|
|
451
|
+
{ type: 'action', actionId: 'manager' },
|
|
452
|
+
]},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
}
|
|
365
458
|
```
|
|
366
459
|
|
|
460
|
+
**Important:** When per-endpoint security is specified, unconfigured endpoints default to `'jwt'` (not `'public'`) for security.
|
|
461
|
+
|
|
367
462
|
---
|
|
368
463
|
|
|
369
464
|
## Decorators
|
|
@@ -373,8 +468,8 @@ export class UserController extends createApiController(
|
|
|
373
468
|
Extract the logged-in user from request:
|
|
374
469
|
|
|
375
470
|
```typescript
|
|
376
|
-
import { CurrentUser } from '@flusys/nestjs-shared
|
|
377
|
-
import { ILoggedUserInfo } from '@flusys/nestjs-shared
|
|
471
|
+
import { CurrentUser } from '@flusys/nestjs-shared';
|
|
472
|
+
import { ILoggedUserInfo } from '@flusys/nestjs-shared';
|
|
378
473
|
|
|
379
474
|
@Controller('profile')
|
|
380
475
|
export class ProfileController {
|
|
@@ -396,7 +491,7 @@ export class ProfileController {
|
|
|
396
491
|
Mark route as public (skip authentication). **Use sparingly - security risk**.
|
|
397
492
|
|
|
398
493
|
```typescript
|
|
399
|
-
import { Public } from '@flusys/nestjs-shared
|
|
494
|
+
import { Public } from '@flusys/nestjs-shared';
|
|
400
495
|
|
|
401
496
|
@Controller('auth')
|
|
402
497
|
export class AuthController {
|
|
@@ -411,7 +506,7 @@ export class AuthController {
|
|
|
411
506
|
Require specific permission(s) - **AND logic** by default:
|
|
412
507
|
|
|
413
508
|
```typescript
|
|
414
|
-
import { RequirePermission } from '@flusys/nestjs-shared
|
|
509
|
+
import { RequirePermission } from '@flusys/nestjs-shared';
|
|
415
510
|
|
|
416
511
|
@Controller('admin')
|
|
417
512
|
export class AdminController {
|
|
@@ -432,7 +527,7 @@ export class AdminController {
|
|
|
432
527
|
Require any of the listed permissions - **OR logic**:
|
|
433
528
|
|
|
434
529
|
```typescript
|
|
435
|
-
import { RequireAnyPermission } from '@flusys/nestjs-shared
|
|
530
|
+
import { RequireAnyPermission } from '@flusys/nestjs-shared';
|
|
436
531
|
|
|
437
532
|
@Controller('reports')
|
|
438
533
|
export class ReportsController {
|
|
@@ -443,22 +538,26 @@ export class ReportsController {
|
|
|
443
538
|
}
|
|
444
539
|
```
|
|
445
540
|
|
|
446
|
-
### @
|
|
541
|
+
### @RequirePermissionLogic
|
|
447
542
|
|
|
448
|
-
Build complex permission
|
|
543
|
+
Build complex permission logic with ILogicNode tree (matches frontend ILogicNode):
|
|
449
544
|
|
|
450
545
|
```typescript
|
|
451
|
-
import {
|
|
546
|
+
import { RequirePermissionLogic } from '@flusys/nestjs-shared';
|
|
452
547
|
|
|
453
548
|
@Controller('sensitive')
|
|
454
549
|
export class SensitiveController {
|
|
455
550
|
// Complex: User needs 'users.read' AND ('admin' OR 'manager')
|
|
456
|
-
@
|
|
457
|
-
|
|
458
|
-
|
|
551
|
+
@RequirePermissionLogic({
|
|
552
|
+
type: 'group',
|
|
553
|
+
operator: 'AND',
|
|
459
554
|
children: [
|
|
460
|
-
{
|
|
461
|
-
|
|
555
|
+
{ type: 'action', actionId: 'users.read' },
|
|
556
|
+
{ type: 'group', operator: 'OR', children: [
|
|
557
|
+
{ type: 'action', actionId: 'admin' },
|
|
558
|
+
{ type: 'action', actionId: 'manager' },
|
|
559
|
+
]},
|
|
560
|
+
],
|
|
462
561
|
})
|
|
463
562
|
@Post('complex')
|
|
464
563
|
getComplexData() { }
|
|
@@ -470,7 +569,7 @@ export class SensitiveController {
|
|
|
470
569
|
Escape HTML entities for XSS prevention:
|
|
471
570
|
|
|
472
571
|
```typescript
|
|
473
|
-
import { SanitizeHtml, SanitizeAndTrim } from '@flusys/nestjs-shared
|
|
572
|
+
import { SanitizeHtml, SanitizeAndTrim } from '@flusys/nestjs-shared';
|
|
474
573
|
|
|
475
574
|
export class CreateCommentDto {
|
|
476
575
|
@SanitizeHtml()
|
|
@@ -488,7 +587,7 @@ export class CreateCommentDto {
|
|
|
488
587
|
Generates Swagger schema for response:
|
|
489
588
|
|
|
490
589
|
```typescript
|
|
491
|
-
import { ApiResponseDto } from '@flusys/nestjs-shared
|
|
590
|
+
import { ApiResponseDto } from '@flusys/nestjs-shared';
|
|
492
591
|
|
|
493
592
|
@Controller('users')
|
|
494
593
|
export class UserController {
|
|
@@ -511,29 +610,30 @@ export class UserController {
|
|
|
511
610
|
Validates JWT tokens for protected routes. Extends Passport's `AuthGuard('jwt')` and respects `@Public()` decorator.
|
|
512
611
|
|
|
513
612
|
```typescript
|
|
514
|
-
import { JwtAuthGuard } from '@flusys/nestjs-shared
|
|
613
|
+
import { JwtAuthGuard } from '@flusys/nestjs-shared';
|
|
614
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
515
615
|
|
|
516
|
-
// Apply globally
|
|
616
|
+
// Apply globally
|
|
517
617
|
@Module({
|
|
518
618
|
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
|
|
519
619
|
})
|
|
520
620
|
export class AppModule {}
|
|
521
621
|
```
|
|
522
622
|
|
|
523
|
-
**Important:** Constructor needs `@Inject(Reflector)` for bundled code.
|
|
623
|
+
**Important:** Constructor needs `@Inject(Reflector)` for bundled code (this is already handled in the guard).
|
|
524
624
|
|
|
525
625
|
### PermissionGuard
|
|
526
626
|
|
|
527
|
-
Checks user permissions from cache with AND/OR/nested logic support.
|
|
627
|
+
Checks user permissions from cache with AND/OR/nested logic support and wildcard matching.
|
|
528
628
|
|
|
529
629
|
```typescript
|
|
530
|
-
import { PermissionGuard } from '@flusys/nestjs-shared
|
|
630
|
+
import { PermissionGuard, PERMISSION_GUARD_CONFIG, CACHE_INSTANCE } from '@flusys/nestjs-shared';
|
|
531
631
|
|
|
532
632
|
@Module({
|
|
533
633
|
providers: [
|
|
534
634
|
{ provide: APP_GUARD, useClass: PermissionGuard },
|
|
535
635
|
{
|
|
536
|
-
provide:
|
|
636
|
+
provide: PERMISSION_GUARD_CONFIG,
|
|
537
637
|
useValue: {
|
|
538
638
|
enableCompanyFeature: true,
|
|
539
639
|
userPermissionKeyFormat: 'permissions:user:{userId}',
|
|
@@ -545,24 +645,85 @@ import { PermissionGuard } from '@flusys/nestjs-shared/guards';
|
|
|
545
645
|
export class AppModule {}
|
|
546
646
|
```
|
|
547
647
|
|
|
648
|
+
**PermissionGuardConfig Interface:**
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
interface PermissionGuardConfig {
|
|
652
|
+
enableCompanyFeature?: boolean;
|
|
653
|
+
userPermissionKeyFormat?: string; // Default: 'permissions:user:{userId}'
|
|
654
|
+
companyPermissionKeyFormat?: string; // Default: 'permissions:company:{companyId}:branch:{branchId}:user:{userId}'
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
548
658
|
**Cache Key Formats:**
|
|
549
659
|
|
|
550
660
|
```typescript
|
|
551
|
-
// Without company feature
|
|
552
|
-
|
|
661
|
+
// Without company feature (enableCompanyFeature: false)
|
|
662
|
+
'permissions:user:{userId}'
|
|
663
|
+
|
|
664
|
+
// With company feature (enableCompanyFeature: true && user.companyId exists)
|
|
665
|
+
'permissions:company:{companyId}:branch:{branchId}:user:{userId}'
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Wildcard Permission Matching:**
|
|
669
|
+
|
|
670
|
+
The guard supports wildcard permissions for flexible access control:
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// User has permissions: ['user.*', 'admin']
|
|
674
|
+
|
|
675
|
+
// These checks will PASS:
|
|
676
|
+
@RequirePermission('user.create') // Matches 'user.*'
|
|
677
|
+
@RequirePermission('user.read') // Matches 'user.*'
|
|
678
|
+
@RequirePermission('admin') // Exact match
|
|
553
679
|
|
|
554
|
-
//
|
|
555
|
-
|
|
680
|
+
// Wildcard patterns:
|
|
681
|
+
'*' // Matches ALL permissions (super admin)
|
|
682
|
+
'user.*' // Matches 'user.create', 'user.read', 'user.update', etc.
|
|
683
|
+
'storage.*' // Matches 'storage.file.create', 'storage.folder.read', etc.
|
|
556
684
|
```
|
|
557
685
|
|
|
558
686
|
### Permission Exceptions
|
|
559
687
|
|
|
560
688
|
```typescript
|
|
561
689
|
import {
|
|
562
|
-
InsufficientPermissionsException,
|
|
563
|
-
NoPermissionsFoundException,
|
|
564
|
-
PermissionSystemUnavailableException,
|
|
565
|
-
} from '@flusys/nestjs-shared
|
|
690
|
+
InsufficientPermissionsException, // 403 - Missing required permissions
|
|
691
|
+
NoPermissionsFoundException, // 403 - No permissions in cache for user
|
|
692
|
+
PermissionSystemUnavailableException, // 500 - Cache not available
|
|
693
|
+
} from '@flusys/nestjs-shared';
|
|
694
|
+
|
|
695
|
+
// Exception response formats:
|
|
696
|
+
// InsufficientPermissionsException (AND):
|
|
697
|
+
{
|
|
698
|
+
"success": false,
|
|
699
|
+
"message": "Missing required permissions: user.create, user.update",
|
|
700
|
+
"code": "INSUFFICIENT_PERMISSIONS",
|
|
701
|
+
"missingPermissions": ["user.create", "user.update"],
|
|
702
|
+
"operator": "AND"
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// InsufficientPermissionsException (OR):
|
|
706
|
+
{
|
|
707
|
+
"success": false,
|
|
708
|
+
"message": "Requires at least one of: admin, manager",
|
|
709
|
+
"code": "INSUFFICIENT_PERMISSIONS",
|
|
710
|
+
"missingPermissions": ["admin", "manager"],
|
|
711
|
+
"operator": "OR"
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// NoPermissionsFoundException:
|
|
715
|
+
{
|
|
716
|
+
"success": false,
|
|
717
|
+
"message": "No permissions found. Please contact administrator.",
|
|
718
|
+
"code": "NO_PERMISSIONS_FOUND"
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// PermissionSystemUnavailableException:
|
|
722
|
+
{
|
|
723
|
+
"success": false,
|
|
724
|
+
"message": "Permission system temporarily unavailable. Please try again later.",
|
|
725
|
+
"code": "PERMISSION_SYSTEM_UNAVAILABLE"
|
|
726
|
+
}
|
|
566
727
|
```
|
|
567
728
|
|
|
568
729
|
---
|
|
@@ -571,20 +732,24 @@ import {
|
|
|
571
732
|
|
|
572
733
|
### LoggerMiddleware
|
|
573
734
|
|
|
574
|
-
Combined middleware for request correlation and HTTP logging.
|
|
735
|
+
Combined middleware for request correlation and HTTP logging using AsyncLocalStorage.
|
|
575
736
|
|
|
576
737
|
**Features:**
|
|
577
738
|
- Request ID generation/tracking (UUID or from `x-request-id` header)
|
|
578
739
|
- Tenant ID tracking (from `x-tenant-id` header)
|
|
579
|
-
- AsyncLocalStorage context for thread-safe access
|
|
740
|
+
- AsyncLocalStorage context for thread-safe access across async operations
|
|
580
741
|
- Automatic sensitive header redaction (authorization, cookie, x-api-key)
|
|
581
742
|
- Performance monitoring (warns on requests > 3s)
|
|
582
|
-
- Body truncation (max 1000 chars)
|
|
743
|
+
- Body truncation (max 1000 chars in logs)
|
|
744
|
+
- Excluded paths: `/health`, `/metrics`, `/favicon.ico`
|
|
745
|
+
|
|
746
|
+
**Configuration via environment:**
|
|
747
|
+
- `LOG_LEVEL` - Logging level (debug enables verbose logging)
|
|
583
748
|
|
|
584
749
|
**Usage:**
|
|
585
750
|
|
|
586
751
|
```typescript
|
|
587
|
-
import { LoggerMiddleware } from '@flusys/nestjs-shared
|
|
752
|
+
import { LoggerMiddleware } from '@flusys/nestjs-shared';
|
|
588
753
|
|
|
589
754
|
@Module({})
|
|
590
755
|
export class AppModule implements NestModule {
|
|
@@ -594,7 +759,19 @@ export class AppModule implements NestModule {
|
|
|
594
759
|
}
|
|
595
760
|
```
|
|
596
761
|
|
|
597
|
-
**
|
|
762
|
+
**Request Context Interface:**
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
interface IRequestContext {
|
|
766
|
+
requestId: string;
|
|
767
|
+
tenantId?: string;
|
|
768
|
+
userId?: string;
|
|
769
|
+
companyId?: string;
|
|
770
|
+
startTime: number;
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**Accessing Request Context (read-only):**
|
|
598
775
|
|
|
599
776
|
```typescript
|
|
600
777
|
import {
|
|
@@ -602,34 +779,37 @@ import {
|
|
|
602
779
|
getTenantId,
|
|
603
780
|
getUserId,
|
|
604
781
|
getCompanyId,
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
} from '@flusys/nestjs-shared/middlewares';
|
|
782
|
+
requestContext, // AsyncLocalStorage instance for advanced usage
|
|
783
|
+
} from '@flusys/nestjs-shared';
|
|
608
784
|
|
|
609
785
|
@Injectable()
|
|
610
786
|
export class MyService {
|
|
611
787
|
async doSomething() {
|
|
612
|
-
const requestId = getRequestId();
|
|
613
|
-
const tenantId = getTenantId();
|
|
614
|
-
|
|
788
|
+
const requestId = getRequestId(); // Current request's correlation ID
|
|
789
|
+
const tenantId = getTenantId(); // From x-tenant-id header
|
|
790
|
+
const userId = getUserId(); // Set by auth guard (if available)
|
|
791
|
+
const companyId = getCompanyId(); // Set by auth (if available)
|
|
615
792
|
}
|
|
616
793
|
}
|
|
617
794
|
```
|
|
618
795
|
|
|
796
|
+
**Note:** The context values (`userId`, `companyId`) are populated by authentication guards after JWT validation. The middleware only initializes `requestId` and `tenantId` from headers.
|
|
797
|
+
|
|
619
798
|
---
|
|
620
799
|
|
|
621
800
|
## Interceptors
|
|
622
801
|
|
|
623
802
|
### ResponseMetaInterceptor
|
|
624
803
|
|
|
625
|
-
|
|
804
|
+
Automatically adds `_meta` to all responses with `success` field:
|
|
626
805
|
|
|
627
806
|
```typescript
|
|
628
|
-
// Response
|
|
807
|
+
// Response structure:
|
|
629
808
|
{
|
|
809
|
+
"success": true,
|
|
630
810
|
"data": [...],
|
|
631
811
|
"_meta": {
|
|
632
|
-
"requestId": "
|
|
812
|
+
"requestId": "req_abc123def456",
|
|
633
813
|
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
634
814
|
"responseTime": 45
|
|
635
815
|
}
|
|
@@ -638,23 +818,92 @@ Adds `_meta` to all responses:
|
|
|
638
818
|
|
|
639
819
|
### IdempotencyInterceptor
|
|
640
820
|
|
|
641
|
-
Prevents duplicate POST requests using `X-Idempotency-Key` header.
|
|
821
|
+
Prevents duplicate POST requests using `X-Idempotency-Key` header.
|
|
822
|
+
|
|
823
|
+
**How it works:**
|
|
824
|
+
1. If key exists and completed → returns cached response (no reprocessing)
|
|
825
|
+
2. If key exists but processing → throws `ConflictException` (409)
|
|
826
|
+
3. If key is new → processes request and caches response for 24 hours
|
|
827
|
+
|
|
828
|
+
**Usage:**
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// Client includes header:
|
|
832
|
+
// X-Idempotency-Key: unique-order-123
|
|
833
|
+
|
|
834
|
+
// Controller (auto-applied by createApiController for insert endpoints)
|
|
835
|
+
@UseInterceptors(IdempotencyInterceptor)
|
|
836
|
+
@Post('create-order')
|
|
837
|
+
createOrder(@Body() dto: CreateOrderDto) { }
|
|
838
|
+
```
|
|
642
839
|
|
|
643
840
|
### SetCreatedByOnBody / SetUpdateByOnBody / SetDeletedByOnBody
|
|
644
841
|
|
|
645
842
|
Auto-set audit user IDs on request body from authenticated user.
|
|
646
843
|
|
|
844
|
+
```typescript
|
|
845
|
+
// Factory function for custom field names
|
|
846
|
+
import { createSetUserFieldInterceptor } from '@flusys/nestjs-shared';
|
|
847
|
+
|
|
848
|
+
// Built-in interceptors:
|
|
849
|
+
export const SetCreatedByOnBody = createSetUserFieldInterceptor('createdById');
|
|
850
|
+
export const SetUpdateByOnBody = createSetUserFieldInterceptor('updatedById');
|
|
851
|
+
export const SetDeletedByOnBody = createSetUserFieldInterceptor('deletedById');
|
|
852
|
+
|
|
853
|
+
// Usage:
|
|
854
|
+
@UseInterceptors(SetCreatedByOnBody)
|
|
855
|
+
@Post('insert')
|
|
856
|
+
insert(@Body() dto: CreateDto) { }
|
|
857
|
+
|
|
858
|
+
// Works with arrays too:
|
|
859
|
+
// Input: [{ name: 'A' }, { name: 'B' }]
|
|
860
|
+
// Output: [{ name: 'A', createdById: 'user-123' }, { name: 'B', createdById: 'user-123' }]
|
|
861
|
+
```
|
|
862
|
+
|
|
647
863
|
### DeleteEmptyIdFromBodyInterceptor
|
|
648
864
|
|
|
649
|
-
Removes empty `id` fields from request body (single and
|
|
865
|
+
Removes empty/null `id` fields from request body (single objects and arrays).
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
// Input: { id: '', name: 'Test' }
|
|
869
|
+
// Output: { name: 'Test' }
|
|
870
|
+
|
|
871
|
+
// Input: [{ id: null, name: 'A' }, { id: '123', name: 'B' }]
|
|
872
|
+
// Output: [{ name: 'A' }, { id: '123', name: 'B' }]
|
|
873
|
+
```
|
|
650
874
|
|
|
651
875
|
### QueryPerformanceInterceptor
|
|
652
876
|
|
|
653
|
-
Monitors execution time and
|
|
877
|
+
Monitors endpoint execution time and logs warnings for slow requests.
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
import { QueryPerformanceInterceptor } from '@flusys/nestjs-shared';
|
|
881
|
+
|
|
882
|
+
// Default threshold: 1000ms
|
|
883
|
+
app.useGlobalInterceptors(new QueryPerformanceInterceptor());
|
|
884
|
+
|
|
885
|
+
// Custom threshold: 500ms
|
|
886
|
+
app.useGlobalInterceptors(new QueryPerformanceInterceptor(500));
|
|
887
|
+
```
|
|
654
888
|
|
|
655
889
|
### Slug Interceptor
|
|
656
890
|
|
|
657
|
-
Auto-generates `slug` from `name` field
|
|
891
|
+
Auto-generates `slug` from `name` field if not provided.
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
import { Slug } from '@flusys/nestjs-shared';
|
|
895
|
+
|
|
896
|
+
@UseInterceptors(Slug)
|
|
897
|
+
@Post('insert')
|
|
898
|
+
insert(@Body() dto: CreateDto) { }
|
|
899
|
+
|
|
900
|
+
// Input: { name: 'My Product Name' }
|
|
901
|
+
// Output: { name: 'My Product Name', slug: 'my-product-name' }
|
|
902
|
+
|
|
903
|
+
// If slug already provided, it's preserved:
|
|
904
|
+
// Input: { name: 'My Product', slug: 'custom-slug' }
|
|
905
|
+
// Output: { name: 'My Product', slug: 'custom-slug' }
|
|
906
|
+
```
|
|
658
907
|
|
|
659
908
|
---
|
|
660
909
|
|
|
@@ -665,7 +914,7 @@ Auto-generates `slug` from `name` field using `UtilsService.transformToSlug()`.
|
|
|
665
914
|
Two-tier caching with in-memory (L1) and Redis (L2):
|
|
666
915
|
|
|
667
916
|
```typescript
|
|
668
|
-
import { HybridCache } from '@flusys/nestjs-shared
|
|
917
|
+
import { HybridCache } from '@flusys/nestjs-shared';
|
|
669
918
|
|
|
670
919
|
@Injectable()
|
|
671
920
|
export class MyService {
|
|
@@ -695,7 +944,7 @@ export class MyService {
|
|
|
695
944
|
### CacheModule Setup
|
|
696
945
|
|
|
697
946
|
```typescript
|
|
698
|
-
import { CacheModule } from '@flusys/nestjs-shared
|
|
947
|
+
import { CacheModule } from '@flusys/nestjs-shared';
|
|
699
948
|
|
|
700
949
|
@Module({
|
|
701
950
|
imports: [
|
|
@@ -723,7 +972,7 @@ Dynamic database connection management with connection pooling.
|
|
|
723
972
|
### Setup
|
|
724
973
|
|
|
725
974
|
```typescript
|
|
726
|
-
import { DataSourceModule } from '@flusys/nestjs-shared
|
|
975
|
+
import { DataSourceModule } from '@flusys/nestjs-shared';
|
|
727
976
|
|
|
728
977
|
@Module({
|
|
729
978
|
imports: [
|
|
@@ -766,7 +1015,7 @@ export class AppModule {}
|
|
|
766
1015
|
### FilterAndPaginationDto
|
|
767
1016
|
|
|
768
1017
|
```typescript
|
|
769
|
-
import { FilterAndPaginationDto } from '@flusys/nestjs-shared
|
|
1018
|
+
import { FilterAndPaginationDto } from '@flusys/nestjs-shared';
|
|
770
1019
|
|
|
771
1020
|
// Request body
|
|
772
1021
|
{
|
|
@@ -781,7 +1030,7 @@ import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
|
|
|
781
1030
|
### DeleteDto
|
|
782
1031
|
|
|
783
1032
|
```typescript
|
|
784
|
-
import { DeleteDto } from '@flusys/nestjs-shared
|
|
1033
|
+
import { DeleteDto } from '@flusys/nestjs-shared';
|
|
785
1034
|
|
|
786
1035
|
// Soft delete
|
|
787
1036
|
{ "id": "uuid", "type": "delete" }
|
|
@@ -842,7 +1091,7 @@ class MessageResponseDto {
|
|
|
842
1091
|
Base entity with UUID and timestamps:
|
|
843
1092
|
|
|
844
1093
|
```typescript
|
|
845
|
-
import { Identity } from '@flusys/nestjs-shared
|
|
1094
|
+
import { Identity } from '@flusys/nestjs-shared';
|
|
846
1095
|
|
|
847
1096
|
@Entity()
|
|
848
1097
|
export class Product extends Identity {
|
|
@@ -859,7 +1108,7 @@ export class Product extends Identity {
|
|
|
859
1108
|
Base user entity with common fields:
|
|
860
1109
|
|
|
861
1110
|
```typescript
|
|
862
|
-
import { UserRoot } from '@flusys/nestjs-shared
|
|
1111
|
+
import { UserRoot } from '@flusys/nestjs-shared';
|
|
863
1112
|
|
|
864
1113
|
@Entity()
|
|
865
1114
|
export class User extends UserRoot {
|
|
@@ -882,7 +1131,7 @@ import {
|
|
|
882
1131
|
buildCompanyWhereCondition,
|
|
883
1132
|
hasCompanyId,
|
|
884
1133
|
validateCompanyOwnership,
|
|
885
|
-
} from '@flusys/nestjs-shared
|
|
1134
|
+
} from '@flusys/nestjs-shared';
|
|
886
1135
|
|
|
887
1136
|
// Add company filter to TypeORM query
|
|
888
1137
|
applyCompanyFilter(query, {
|
|
@@ -900,7 +1149,7 @@ validateCompanyOwnership(entity, user, 'Entity');
|
|
|
900
1149
|
### String Utilities
|
|
901
1150
|
|
|
902
1151
|
```typescript
|
|
903
|
-
import { generateSlug, generateUniqueSlug } from '@flusys/nestjs-shared
|
|
1152
|
+
import { generateSlug, generateUniqueSlug } from '@flusys/nestjs-shared';
|
|
904
1153
|
|
|
905
1154
|
// Generate URL-friendly slug
|
|
906
1155
|
const slug = generateSlug('My Product Name', 100);
|
|
@@ -921,7 +1170,7 @@ import {
|
|
|
921
1170
|
isBrowserRequest,
|
|
922
1171
|
buildCookieOptions,
|
|
923
1172
|
parseDurationToMs,
|
|
924
|
-
} from '@flusys/nestjs-shared
|
|
1173
|
+
} from '@flusys/nestjs-shared';
|
|
925
1174
|
|
|
926
1175
|
// Detect browser vs API client
|
|
927
1176
|
const isBrowser = isBrowserRequest(req);
|
|
@@ -936,7 +1185,7 @@ const ms = parseDurationToMs('7d'); // 604800000
|
|
|
936
1185
|
### HTML Sanitizer
|
|
937
1186
|
|
|
938
1187
|
```typescript
|
|
939
|
-
import { escapeHtml, escapeHtmlVariables } from '@flusys/nestjs-shared
|
|
1188
|
+
import { escapeHtml, escapeHtmlVariables } from '@flusys/nestjs-shared';
|
|
940
1189
|
|
|
941
1190
|
// Escape single string
|
|
942
1191
|
const safe = escapeHtml('<script>alert("xss")</script>');
|
|
@@ -953,68 +1202,228 @@ const safeVars = escapeHtmlVariables({
|
|
|
953
1202
|
|
|
954
1203
|
## Error Handling
|
|
955
1204
|
|
|
956
|
-
###
|
|
1205
|
+
### ErrorHandler Class
|
|
1206
|
+
|
|
1207
|
+
Centralized error handling with automatic sensitive data redaction.
|
|
957
1208
|
|
|
958
1209
|
```typescript
|
|
959
|
-
import {
|
|
960
|
-
|
|
961
|
-
logError,
|
|
962
|
-
rethrowError,
|
|
963
|
-
logAndRethrow,
|
|
964
|
-
} from '@flusys/nestjs-shared/utils';
|
|
1210
|
+
import { ErrorHandler, IErrorContext } from '@flusys/nestjs-shared';
|
|
1211
|
+
import { Logger } from '@nestjs/common';
|
|
965
1212
|
|
|
1213
|
+
const logger = new Logger('MyService');
|
|
1214
|
+
|
|
1215
|
+
// Error context interface
|
|
966
1216
|
interface IErrorContext {
|
|
967
1217
|
operation?: string;
|
|
968
1218
|
entity?: string;
|
|
969
1219
|
userId?: string;
|
|
970
1220
|
id?: string;
|
|
971
1221
|
companyId?: string;
|
|
1222
|
+
branchId?: string;
|
|
1223
|
+
sectionId?: string;
|
|
972
1224
|
data?: Record<string, unknown>;
|
|
973
1225
|
}
|
|
974
1226
|
|
|
975
|
-
// Safe error message extraction
|
|
976
|
-
const message = getErrorMessage(error);
|
|
977
|
-
|
|
978
|
-
// Log with sensitive key redaction (password, secret, token, apiKey)
|
|
979
|
-
logError(logger, error, 'createUser', { userId: user.id });
|
|
1227
|
+
// Safe error message extraction from unknown error
|
|
1228
|
+
const message = ErrorHandler.getErrorMessage(error);
|
|
1229
|
+
// Returns: 'Error message' | 'Unknown error occurred'
|
|
980
1230
|
|
|
981
|
-
//
|
|
982
|
-
|
|
1231
|
+
// Log error with sensitive key redaction
|
|
1232
|
+
// Automatically redacts: password, secret, token, apiKey, credential, authorization
|
|
1233
|
+
ErrorHandler.logError(logger, error, 'createUser', {
|
|
1234
|
+
entity: 'User',
|
|
1235
|
+
userId: user.id,
|
|
1236
|
+
data: { email: 'test@example.com', password: 'secret123' }, // password will be [REDACTED]
|
|
1237
|
+
});
|
|
983
1238
|
|
|
984
|
-
//
|
|
985
|
-
|
|
1239
|
+
// Type-safe rethrow (preserves Error instances, wraps others)
|
|
1240
|
+
ErrorHandler.rethrowError(error); // throws: never
|
|
1241
|
+
|
|
1242
|
+
// Combined log + rethrow (common pattern)
|
|
1243
|
+
ErrorHandler.logAndRethrow(logger, error, 'updateUser', { entity: 'User', id: userId });
|
|
1244
|
+
|
|
1245
|
+
// Typical usage in service
|
|
1246
|
+
async createUser(dto: CreateUserDto): Promise<User> {
|
|
1247
|
+
try {
|
|
1248
|
+
return await this.repository.save(dto);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
ErrorHandler.logAndRethrow(this.logger, error, 'createUser', {
|
|
1251
|
+
entity: 'User',
|
|
1252
|
+
data: dto,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
986
1256
|
```
|
|
987
1257
|
|
|
1258
|
+
**Sensitive Keys (automatically redacted in logs):**
|
|
1259
|
+
- `password`
|
|
1260
|
+
- `secret`
|
|
1261
|
+
- `token`
|
|
1262
|
+
- `apiKey`
|
|
1263
|
+
- `credential`
|
|
1264
|
+
- `authorization`
|
|
1265
|
+
|
|
988
1266
|
---
|
|
989
1267
|
|
|
990
1268
|
## Constants
|
|
991
1269
|
|
|
1270
|
+
### Metadata Keys
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from '@flusys/nestjs-shared';
|
|
1274
|
+
|
|
1275
|
+
// Used internally by decorators:
|
|
1276
|
+
IS_PUBLIC_KEY // 'isPublic' - marks routes as public
|
|
1277
|
+
PERMISSIONS_KEY // 'permissions' - stores permission requirements
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
### Injection Tokens
|
|
1281
|
+
|
|
1282
|
+
```typescript
|
|
1283
|
+
import {
|
|
1284
|
+
CACHE_INSTANCE,
|
|
1285
|
+
PERMISSION_GUARD_CONFIG,
|
|
1286
|
+
LOGGER_INSTANCE,
|
|
1287
|
+
} from '@flusys/nestjs-shared';
|
|
1288
|
+
|
|
1289
|
+
// Injection tokens:
|
|
1290
|
+
CACHE_INSTANCE // 'CACHE_INSTANCE' - HybridCache provider
|
|
1291
|
+
PERMISSION_GUARD_CONFIG // 'PERMISSION_GUARD_CONFIG' - PermissionGuard config
|
|
1292
|
+
LOGGER_INSTANCE // 'LOGGER_INSTANCE' - ILogger provider
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
### Header Names
|
|
1296
|
+
|
|
1297
|
+
```typescript
|
|
1298
|
+
import {
|
|
1299
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
1300
|
+
REQUEST_ID_HEADER,
|
|
1301
|
+
CLIENT_TYPE_HEADER,
|
|
1302
|
+
} from '@flusys/nestjs-shared';
|
|
1303
|
+
|
|
1304
|
+
// HTTP header names:
|
|
1305
|
+
IDEMPOTENCY_KEY_HEADER // 'x-idempotency-key' - For IdempotencyInterceptor
|
|
1306
|
+
REQUEST_ID_HEADER // 'x-request-id' - For request correlation
|
|
1307
|
+
CLIENT_TYPE_HEADER // 'x-client-type' - For client detection (browser/api)
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
### Cache Key Prefixes
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
import {
|
|
1314
|
+
PERMISSIONS_CACHE_PREFIX,
|
|
1315
|
+
IDEMPOTENCY_CACHE_PREFIX,
|
|
1316
|
+
} from '@flusys/nestjs-shared';
|
|
1317
|
+
|
|
1318
|
+
// Cache key prefixes:
|
|
1319
|
+
PERMISSIONS_CACHE_PREFIX // 'permissions' - For user permissions cache
|
|
1320
|
+
IDEMPOTENCY_CACHE_PREFIX // 'idempotency' - For idempotency keys
|
|
1321
|
+
```
|
|
1322
|
+
|
|
992
1323
|
### Permission Constants
|
|
993
1324
|
|
|
994
1325
|
```typescript
|
|
995
|
-
import { PERMISSIONS } from '@flusys/nestjs-shared
|
|
1326
|
+
import { PERMISSIONS, PermissionCode } from '@flusys/nestjs-shared';
|
|
996
1327
|
|
|
997
|
-
//
|
|
1328
|
+
// Auth Module
|
|
998
1329
|
PERMISSIONS.USER.CREATE // 'user.create'
|
|
999
1330
|
PERMISSIONS.USER.READ // 'user.read'
|
|
1000
1331
|
PERMISSIONS.USER.UPDATE // 'user.update'
|
|
1001
1332
|
PERMISSIONS.USER.DELETE // 'user.delete'
|
|
1002
1333
|
|
|
1003
1334
|
PERMISSIONS.COMPANY.CREATE // 'company.create'
|
|
1335
|
+
PERMISSIONS.COMPANY.READ // 'company.read'
|
|
1336
|
+
// ...
|
|
1337
|
+
|
|
1004
1338
|
PERMISSIONS.BRANCH.CREATE // 'branch.create'
|
|
1339
|
+
// ...
|
|
1005
1340
|
|
|
1341
|
+
// IAM Module
|
|
1342
|
+
PERMISSIONS.ACTION.CREATE // 'action.create'
|
|
1006
1343
|
PERMISSIONS.ROLE.CREATE // 'role.create'
|
|
1344
|
+
PERMISSIONS.ROLE_ACTION.READ // 'role-action.read'
|
|
1345
|
+
PERMISSIONS.ROLE_ACTION.ASSIGN // 'role-action.assign'
|
|
1346
|
+
PERMISSIONS.USER_ROLE.READ // 'user-role.read'
|
|
1347
|
+
PERMISSIONS.USER_ROLE.ASSIGN // 'user-role.assign'
|
|
1348
|
+
PERMISSIONS.USER_ACTION.READ // 'user-action.read'
|
|
1349
|
+
PERMISSIONS.USER_ACTION.ASSIGN // 'user-action.assign'
|
|
1350
|
+
PERMISSIONS.COMPANY_ACTION.READ // 'company-action.read'
|
|
1351
|
+
PERMISSIONS.COMPANY_ACTION.ASSIGN // 'company-action.assign'
|
|
1352
|
+
|
|
1353
|
+
// Storage Module
|
|
1007
1354
|
PERMISSIONS.FILE.CREATE // 'file.create'
|
|
1008
1355
|
PERMISSIONS.FOLDER.CREATE // 'folder.create'
|
|
1009
|
-
//
|
|
1356
|
+
PERMISSIONS.STORAGE_CONFIG.CREATE // 'storage-config.create'
|
|
1357
|
+
|
|
1358
|
+
// Email Module
|
|
1359
|
+
PERMISSIONS.EMAIL_CONFIG.CREATE // 'email-config.create'
|
|
1360
|
+
PERMISSIONS.EMAIL_TEMPLATE.CREATE // 'email-template.create'
|
|
1361
|
+
|
|
1362
|
+
// Form Builder Module
|
|
1363
|
+
PERMISSIONS.FORM.CREATE // 'form.create'
|
|
1364
|
+
PERMISSIONS.FORM_RESULT.CREATE // 'form-result.create'
|
|
1365
|
+
|
|
1366
|
+
// Type-safe permission code type:
|
|
1367
|
+
type PermissionCode = 'user.create' | 'user.read' | ... ; // Union of all permission strings
|
|
1010
1368
|
```
|
|
1011
1369
|
|
|
1012
|
-
|
|
1370
|
+
---
|
|
1371
|
+
|
|
1372
|
+
## Logger System
|
|
1373
|
+
|
|
1374
|
+
### Winston Logger
|
|
1375
|
+
|
|
1376
|
+
Production-ready logging with tenant-aware routing and daily rotation.
|
|
1377
|
+
|
|
1378
|
+
**Configuration via environment:**
|
|
1379
|
+
- `LOG_DIR` - Directory for log files (default: `logs/`)
|
|
1380
|
+
- `LOG_LEVEL` - Minimum log level (default: `info`)
|
|
1381
|
+
- `LOG_MAX_SIZE` - Max file size before rotation
|
|
1382
|
+
- `LOG_MAX_FILES` - Max number of log files to keep
|
|
1383
|
+
- `USE_TENANT_MODE` - Enable tenant-aware log routing
|
|
1384
|
+
|
|
1385
|
+
**Logger Modes:**
|
|
1386
|
+
|
|
1387
|
+
```typescript
|
|
1388
|
+
import { instance as winstonLogger } from '@flusys/nestjs-shared';
|
|
1389
|
+
|
|
1390
|
+
// Development: Console output with colors
|
|
1391
|
+
// Production: File-based with daily rotation + tenant routing
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
**Log File Structure:**
|
|
1395
|
+
|
|
1396
|
+
```
|
|
1397
|
+
logs/
|
|
1398
|
+
├── combined-2024-01-15.log # All logs (tenant mode off)
|
|
1399
|
+
├── error-2024-01-15.log # Error logs only
|
|
1400
|
+
└── {tenantId}/ # Tenant-specific folders (tenant mode on)
|
|
1401
|
+
└── combined-2024-01-15.log
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### Logger Adapters
|
|
1013
1405
|
|
|
1014
1406
|
```typescript
|
|
1015
|
-
'
|
|
1016
|
-
'
|
|
1017
|
-
|
|
1407
|
+
import { WinstonLoggerAdapter, NestLoggerAdapter, ILogger } from '@flusys/nestjs-shared';
|
|
1408
|
+
import { Logger } from '@nestjs/common';
|
|
1409
|
+
|
|
1410
|
+
// ILogger interface
|
|
1411
|
+
interface ILogger {
|
|
1412
|
+
log(message: string, context?: string, ...args: any[]): void;
|
|
1413
|
+
error(message: string, trace?: string, context?: string, ...args: any[]): void;
|
|
1414
|
+
warn(message: string, context?: string, ...args: any[]): void;
|
|
1415
|
+
debug(message: string, context?: string, ...args: any[]): void;
|
|
1416
|
+
verbose(message: string, context?: string, ...args: any[]): void;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Winston adapter (recommended for production)
|
|
1420
|
+
// Automatically includes correlation context (requestId, userId, tenantId, companyId)
|
|
1421
|
+
const logger = new WinstonLoggerAdapter('MyService');
|
|
1422
|
+
logger.log('Operation completed', undefined, { orderId: '123' });
|
|
1423
|
+
// Output: { requestId: 'uuid', userId: 'user-id', context: 'MyService', message: 'Operation completed', orderId: '123' }
|
|
1424
|
+
|
|
1425
|
+
// NestJS adapter (for testing or simple use cases)
|
|
1426
|
+
const nestLogger = new NestLoggerAdapter(new Logger('MyService'));
|
|
1018
1427
|
```
|
|
1019
1428
|
|
|
1020
1429
|
---
|
|
@@ -1023,59 +1432,69 @@ PERMISSIONS.FOLDER.CREATE // 'folder.create'
|
|
|
1023
1432
|
|
|
1024
1433
|
### Main Exports
|
|
1025
1434
|
|
|
1435
|
+
All exports are available from the main package entry point:
|
|
1436
|
+
|
|
1026
1437
|
```typescript
|
|
1027
|
-
//
|
|
1438
|
+
// Import everything from main entry
|
|
1028
1439
|
import {
|
|
1440
|
+
// Classes
|
|
1029
1441
|
ApiService,
|
|
1030
1442
|
RequestScopedApiService,
|
|
1031
1443
|
createApiController,
|
|
1032
1444
|
HybridCache,
|
|
1033
1445
|
WinstonLoggerAdapter,
|
|
1034
1446
|
NestLoggerAdapter,
|
|
1035
|
-
|
|
1447
|
+
instance as winstonLogger,
|
|
1036
1448
|
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1449
|
+
// Controller Types
|
|
1450
|
+
ApiEndpoint,
|
|
1451
|
+
SecurityLevel,
|
|
1452
|
+
EndpointSecurity,
|
|
1453
|
+
ApiSecurityConfig,
|
|
1454
|
+
ApiControllerOptions,
|
|
1455
|
+
|
|
1456
|
+
// Decorators
|
|
1039
1457
|
CurrentUser,
|
|
1040
1458
|
Public,
|
|
1041
1459
|
RequirePermission,
|
|
1042
1460
|
RequireAnyPermission,
|
|
1043
|
-
|
|
1461
|
+
RequirePermissionLogic,
|
|
1462
|
+
RequirePermissionCondition, // @deprecated - use RequirePermissionLogic
|
|
1044
1463
|
SanitizeHtml,
|
|
1045
1464
|
SanitizeAndTrim,
|
|
1046
1465
|
ApiResponseDto,
|
|
1047
|
-
|
|
1466
|
+
ArrayResponseType,
|
|
1048
1467
|
|
|
1049
|
-
// Guards
|
|
1050
|
-
|
|
1468
|
+
// Guards
|
|
1469
|
+
JwtAuthGuard,
|
|
1470
|
+
PermissionGuard,
|
|
1051
1471
|
|
|
1052
|
-
// Interceptors
|
|
1053
|
-
import {
|
|
1472
|
+
// Interceptors
|
|
1054
1473
|
ResponseMetaInterceptor,
|
|
1055
1474
|
IdempotencyInterceptor,
|
|
1056
1475
|
SetCreatedByOnBody,
|
|
1057
1476
|
SetUpdateByOnBody,
|
|
1058
1477
|
SetDeletedByOnBody,
|
|
1478
|
+
createSetUserFieldInterceptor, // Factory function
|
|
1059
1479
|
DeleteEmptyIdFromBodyInterceptor,
|
|
1060
1480
|
QueryPerformanceInterceptor,
|
|
1061
1481
|
Slug,
|
|
1062
|
-
} from '@flusys/nestjs-shared/interceptors';
|
|
1063
1482
|
|
|
1064
|
-
// Modules
|
|
1065
|
-
import {
|
|
1483
|
+
// Modules
|
|
1066
1484
|
CacheModule,
|
|
1067
1485
|
DataSourceModule,
|
|
1486
|
+
DataSourceModuleOptions,
|
|
1487
|
+
DataSourceOptionsFactory,
|
|
1488
|
+
DataSourceModuleAsyncOptions,
|
|
1068
1489
|
UtilsModule,
|
|
1069
1490
|
UtilsService,
|
|
1070
1491
|
MultiTenantDataSourceService,
|
|
1071
|
-
} from '@flusys/nestjs-shared/modules';
|
|
1072
1492
|
|
|
1073
|
-
// DTOs
|
|
1074
|
-
import {
|
|
1493
|
+
// DTOs
|
|
1075
1494
|
FilterAndPaginationDto,
|
|
1495
|
+
GetByIdBodyDto,
|
|
1076
1496
|
PaginationDto,
|
|
1077
1497
|
DeleteDto,
|
|
1078
|
-
GetByIdBodyDto,
|
|
1079
1498
|
SingleResponseDto,
|
|
1080
1499
|
ListResponseDto,
|
|
1081
1500
|
BulkResponseDto,
|
|
@@ -1084,58 +1503,137 @@ import {
|
|
|
1084
1503
|
PaginationMetaDto,
|
|
1085
1504
|
BulkMetaDto,
|
|
1086
1505
|
RequestMetaDto,
|
|
1087
|
-
} from '@flusys/nestjs-shared/dtos';
|
|
1088
1506
|
|
|
1089
|
-
// Entities
|
|
1090
|
-
|
|
1507
|
+
// Entities
|
|
1508
|
+
Identity,
|
|
1509
|
+
UserRoot,
|
|
1091
1510
|
|
|
1092
|
-
// Interfaces
|
|
1093
|
-
import {
|
|
1511
|
+
// Interfaces
|
|
1094
1512
|
ILoggedUserInfo,
|
|
1095
1513
|
IService,
|
|
1096
1514
|
IDataSourceProvider,
|
|
1097
1515
|
IModuleConfigService,
|
|
1098
1516
|
ILogger,
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
validateCompanyOwnership,
|
|
1110
|
-
generateSlug,
|
|
1111
|
-
generateUniqueSlug,
|
|
1112
|
-
isBrowserRequest,
|
|
1113
|
-
buildCookieOptions,
|
|
1114
|
-
parseDurationToMs,
|
|
1115
|
-
escapeHtml,
|
|
1116
|
-
escapeHtmlVariables,
|
|
1117
|
-
} from '@flusys/nestjs-shared/utils';
|
|
1118
|
-
|
|
1119
|
-
// Middleware
|
|
1120
|
-
import {
|
|
1517
|
+
IIdentity,
|
|
1518
|
+
ILogicNode,
|
|
1519
|
+
IActionNode,
|
|
1520
|
+
IGroupNode,
|
|
1521
|
+
IPermissionLogic,
|
|
1522
|
+
SimplePermissionConfig,
|
|
1523
|
+
PermissionConfig,
|
|
1524
|
+
PermissionGuardConfig,
|
|
1525
|
+
|
|
1526
|
+
// Middleware
|
|
1121
1527
|
LoggerMiddleware,
|
|
1528
|
+
IRequestContext,
|
|
1529
|
+
requestContext,
|
|
1122
1530
|
getRequestId,
|
|
1123
1531
|
getTenantId,
|
|
1124
1532
|
getUserId,
|
|
1125
1533
|
getCompanyId,
|
|
1126
|
-
setUserId,
|
|
1127
|
-
setCompanyId,
|
|
1128
|
-
} from '@flusys/nestjs-shared/middlewares';
|
|
1129
1534
|
|
|
1130
|
-
// Exceptions
|
|
1131
|
-
import {
|
|
1535
|
+
// Exceptions
|
|
1132
1536
|
InsufficientPermissionsException,
|
|
1133
1537
|
NoPermissionsFoundException,
|
|
1134
1538
|
PermissionSystemUnavailableException,
|
|
1135
|
-
} from '@flusys/nestjs-shared/exceptions';
|
|
1136
1539
|
|
|
1137
|
-
// Constants
|
|
1138
|
-
|
|
1540
|
+
// Constants
|
|
1541
|
+
IS_PUBLIC_KEY,
|
|
1542
|
+
PERMISSIONS_KEY,
|
|
1543
|
+
CACHE_INSTANCE,
|
|
1544
|
+
PERMISSION_GUARD_CONFIG,
|
|
1545
|
+
LOGGER_INSTANCE,
|
|
1546
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
1547
|
+
REQUEST_ID_HEADER,
|
|
1548
|
+
CLIENT_TYPE_HEADER,
|
|
1549
|
+
PERMISSIONS_CACHE_PREFIX,
|
|
1550
|
+
IDEMPOTENCY_CACHE_PREFIX,
|
|
1551
|
+
PERMISSIONS,
|
|
1552
|
+
PermissionCode,
|
|
1553
|
+
// Individual permission exports
|
|
1554
|
+
USER_PERMISSIONS,
|
|
1555
|
+
COMPANY_PERMISSIONS,
|
|
1556
|
+
BRANCH_PERMISSIONS,
|
|
1557
|
+
ACTION_PERMISSIONS,
|
|
1558
|
+
ROLE_PERMISSIONS,
|
|
1559
|
+
ROLE_ACTION_PERMISSIONS,
|
|
1560
|
+
USER_ROLE_PERMISSIONS,
|
|
1561
|
+
USER_ACTION_PERMISSIONS,
|
|
1562
|
+
COMPANY_ACTION_PERMISSIONS,
|
|
1563
|
+
FILE_PERMISSIONS,
|
|
1564
|
+
FOLDER_PERMISSIONS,
|
|
1565
|
+
STORAGE_CONFIG_PERMISSIONS,
|
|
1566
|
+
EMAIL_CONFIG_PERMISSIONS,
|
|
1567
|
+
EMAIL_TEMPLATE_PERMISSIONS,
|
|
1568
|
+
FORM_PERMISSIONS,
|
|
1569
|
+
FORM_RESULT_PERMISSIONS,
|
|
1570
|
+
|
|
1571
|
+
// Utilities
|
|
1572
|
+
ErrorHandler,
|
|
1573
|
+
IErrorContext,
|
|
1574
|
+
escapeHtml,
|
|
1575
|
+
escapeHtmlVariables,
|
|
1576
|
+
applyCompanyFilter,
|
|
1577
|
+
buildCompanyWhereCondition,
|
|
1578
|
+
hasCompanyId,
|
|
1579
|
+
validateCompanyOwnership,
|
|
1580
|
+
ICompanyFilterConfig,
|
|
1581
|
+
ICompanyEnabled,
|
|
1582
|
+
isBrowserRequest,
|
|
1583
|
+
buildCookieOptions,
|
|
1584
|
+
parseDurationToMs,
|
|
1585
|
+
generateSlug,
|
|
1586
|
+
generateUniqueSlug,
|
|
1587
|
+
} from '@flusys/nestjs-shared';
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
---
|
|
1591
|
+
|
|
1592
|
+
## UtilsService
|
|
1593
|
+
|
|
1594
|
+
Global utility service for cache management and string operations.
|
|
1595
|
+
|
|
1596
|
+
```typescript
|
|
1597
|
+
import { UtilsService, CACHE_INSTANCE, HybridCache } from '@flusys/nestjs-shared';
|
|
1598
|
+
|
|
1599
|
+
@Injectable()
|
|
1600
|
+
export class MyService {
|
|
1601
|
+
constructor(
|
|
1602
|
+
@Inject(UtilsService) private readonly utilsService: UtilsService,
|
|
1603
|
+
@Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
|
|
1604
|
+
) {}
|
|
1605
|
+
|
|
1606
|
+
// Cache key generation with optional tenant prefix
|
|
1607
|
+
getCacheKey(entityName: string, params: any, entityId?: string, tenantId?: string): string;
|
|
1608
|
+
// Example: this.utilsService.getCacheKey('user', { filter: {} }, 'user-123', 'tenant-1')
|
|
1609
|
+
// Returns: 'tenant_tenant-1_entity_user_id_user-123_select_{"filter":{}}'
|
|
1610
|
+
|
|
1611
|
+
// Track cache keys for invalidation
|
|
1612
|
+
async trackCacheKey(
|
|
1613
|
+
cacheKey: string,
|
|
1614
|
+
entityName: string,
|
|
1615
|
+
cacheManager: HybridCache,
|
|
1616
|
+
entityId?: string,
|
|
1617
|
+
tenantId?: string,
|
|
1618
|
+
): Promise<void>;
|
|
1619
|
+
|
|
1620
|
+
// Clear all cached entries for an entity
|
|
1621
|
+
async clearCache(
|
|
1622
|
+
entityName: string,
|
|
1623
|
+
cacheManager: HybridCache,
|
|
1624
|
+
entityId?: string,
|
|
1625
|
+
tenantId?: string,
|
|
1626
|
+
): Promise<void>;
|
|
1627
|
+
|
|
1628
|
+
// Generate URL-friendly slug
|
|
1629
|
+
transformToSlug(value: string, salt?: boolean): string;
|
|
1630
|
+
// Example: this.utilsService.transformToSlug('My Product Name')
|
|
1631
|
+
// Returns: 'my-product-name'
|
|
1632
|
+
// With salt: 'my-product-name-42'
|
|
1633
|
+
|
|
1634
|
+
// Generate random integer
|
|
1635
|
+
getRandomInt(min: number, max: number): number;
|
|
1636
|
+
}
|
|
1139
1637
|
```
|
|
1140
1638
|
|
|
1141
1639
|
---
|
|
@@ -1145,58 +1643,136 @@ import { PERMISSIONS } from '@flusys/nestjs-shared/constants';
|
|
|
1145
1643
|
### 1. Use Generic Service Pattern
|
|
1146
1644
|
|
|
1147
1645
|
```typescript
|
|
1148
|
-
// Extend ApiService for consistent CRUD
|
|
1646
|
+
// Extend ApiService for consistent CRUD operations
|
|
1149
1647
|
@Injectable()
|
|
1150
|
-
export class ProductService extends ApiService
|
|
1151
|
-
|
|
1648
|
+
export class ProductService extends ApiService<
|
|
1649
|
+
CreateProductDto,
|
|
1650
|
+
UpdateProductDto,
|
|
1651
|
+
IProduct,
|
|
1652
|
+
Product,
|
|
1653
|
+
Repository<Product>
|
|
1654
|
+
> {
|
|
1655
|
+
// Override hooks for custom business logic
|
|
1656
|
+
protected async beforeInsertOperation(dto, user, qr): Promise<void> {
|
|
1657
|
+
// Validation, transformations, etc.
|
|
1658
|
+
}
|
|
1152
1659
|
}
|
|
1153
1660
|
|
|
1154
|
-
// For dynamic
|
|
1661
|
+
// For dynamic entity resolution, use RequestScopedApiService
|
|
1155
1662
|
@Injectable({ scope: Scope.REQUEST })
|
|
1156
|
-
export class RoleService extends RequestScopedApiService<...> {
|
|
1663
|
+
export class RoleService extends RequestScopedApiService<...> {
|
|
1664
|
+
protected resolveEntity() {
|
|
1665
|
+
return this.config.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1157
1668
|
```
|
|
1158
1669
|
|
|
1159
1670
|
### 2. Always Use @Inject() Decorators
|
|
1160
1671
|
|
|
1161
|
-
Required for esbuild bundled code:
|
|
1672
|
+
Required for esbuild bundled code (NestJS DI metadata may be lost):
|
|
1162
1673
|
|
|
1163
1674
|
```typescript
|
|
1164
|
-
// CORRECT
|
|
1675
|
+
// CORRECT - explicit injection
|
|
1165
1676
|
constructor(
|
|
1166
1677
|
@Inject(MyService) private readonly myService: MyService,
|
|
1167
|
-
@Inject(
|
|
1678
|
+
@Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
|
|
1679
|
+
@Inject(UtilsService) private readonly utils: UtilsService,
|
|
1168
1680
|
) {}
|
|
1169
1681
|
|
|
1170
|
-
// WRONG -
|
|
1682
|
+
// WRONG - may fail in bundled code
|
|
1171
1683
|
constructor(private readonly myService: MyService) {}
|
|
1172
1684
|
```
|
|
1173
1685
|
|
|
1174
1686
|
### 3. Use Decorators Consistently
|
|
1175
1687
|
|
|
1176
1688
|
```typescript
|
|
1177
|
-
// Use built-in decorators
|
|
1689
|
+
// Use built-in decorators for type safety
|
|
1178
1690
|
@CurrentUser() user: ILoggedUserInfo
|
|
1179
|
-
@
|
|
1180
|
-
|
|
1691
|
+
@CurrentUser('id') userId: string // Extract specific property
|
|
1692
|
+
|
|
1693
|
+
// Permission decorators
|
|
1694
|
+
@RequirePermission('user.create') // Single permission (AND)
|
|
1695
|
+
@RequirePermission('user.create', 'admin') // Multiple permissions (AND)
|
|
1696
|
+
@RequireAnyPermission('user.read', 'admin') // Multiple permissions (OR)
|
|
1697
|
+
@RequirePermissionLogic({ type: 'group', ... }) // Complex logic
|
|
1698
|
+
|
|
1699
|
+
// Mark public routes sparingly - security risk!
|
|
1700
|
+
@Public()
|
|
1181
1701
|
|
|
1182
|
-
//
|
|
1183
|
-
@Req() req: Request // Not type-safe
|
|
1702
|
+
// Avoid direct request access - use decorators
|
|
1703
|
+
// @Req() req: Request // Not type-safe, avoid!
|
|
1184
1704
|
```
|
|
1185
1705
|
|
|
1186
1706
|
### 4. Configure Security at Controller Level
|
|
1187
1707
|
|
|
1188
1708
|
```typescript
|
|
1189
|
-
// GOOD -
|
|
1190
|
-
export class UserController extends createApiController(
|
|
1191
|
-
|
|
1192
|
-
|
|
1709
|
+
// GOOD - centralized security config
|
|
1710
|
+
export class UserController extends createApiController(
|
|
1711
|
+
CreateUserDto, UpdateUserDto, UserResponseDto,
|
|
1712
|
+
{
|
|
1713
|
+
security: {
|
|
1714
|
+
getAll: 'jwt',
|
|
1715
|
+
insert: { level: 'permission', permissions: ['user.create'] },
|
|
1716
|
+
update: { level: 'permission', permissions: ['user.update'] },
|
|
1717
|
+
delete: { level: 'permission', permissions: ['user.delete'] },
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
) {}
|
|
1193
1721
|
|
|
1194
|
-
// AVOID -
|
|
1722
|
+
// AVOID - scattered guards on each endpoint
|
|
1195
1723
|
@UseGuards(JwtGuard)
|
|
1196
1724
|
@Post('create')
|
|
1197
1725
|
create() {}
|
|
1198
1726
|
```
|
|
1199
1727
|
|
|
1728
|
+
### 5. Use Permission Constants
|
|
1729
|
+
|
|
1730
|
+
```typescript
|
|
1731
|
+
import { PERMISSIONS } from '@flusys/nestjs-shared';
|
|
1732
|
+
|
|
1733
|
+
// GOOD - use constants for type safety and refactoring
|
|
1734
|
+
@RequirePermission(PERMISSIONS.USER.CREATE)
|
|
1735
|
+
|
|
1736
|
+
// AVOID - hardcoded strings prone to typos
|
|
1737
|
+
@RequirePermission('user.create')
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
### 6. Leverage Company Filtering Utilities
|
|
1741
|
+
|
|
1742
|
+
```typescript
|
|
1743
|
+
import { applyCompanyFilter, validateCompanyOwnership } from '@flusys/nestjs-shared';
|
|
1744
|
+
|
|
1745
|
+
// In service getExtraManipulateQuery hook
|
|
1746
|
+
protected async getExtraManipulateQuery(query, dto, user) {
|
|
1747
|
+
applyCompanyFilter(query, {
|
|
1748
|
+
isCompanyFeatureEnabled: this.config.isCompanyFeatureEnabled(),
|
|
1749
|
+
entityAlias: this.entityName,
|
|
1750
|
+
}, user);
|
|
1751
|
+
return { query, isRaw: false };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Validate ownership before operations
|
|
1755
|
+
validateCompanyOwnership(entity, user, this.config.isCompanyFeatureEnabled(), 'Product');
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
### 7. Error Handling Pattern
|
|
1759
|
+
|
|
1760
|
+
```typescript
|
|
1761
|
+
import { ErrorHandler } from '@flusys/nestjs-shared';
|
|
1762
|
+
|
|
1763
|
+
// Use ErrorHandler for consistent logging with sensitive data redaction
|
|
1764
|
+
async createUser(dto: CreateUserDto): Promise<User> {
|
|
1765
|
+
try {
|
|
1766
|
+
return await this.repository.save(dto);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
ErrorHandler.logAndRethrow(this.logger, error, 'createUser', {
|
|
1769
|
+
entity: 'User',
|
|
1770
|
+
data: dto, // Sensitive fields auto-redacted
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1200
1776
|
---
|
|
1201
1777
|
|
|
1202
|
-
**Last Updated:** 2026-02-
|
|
1778
|
+
**Last Updated:** 2026-02-25
|