@flusys/nestjs-shared 1.0.0-rc → 2.0.0-rc.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 +493 -658
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/permissions.js +11 -1
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/response-payload.dto.js +0 -75
- package/cjs/guards/permission.guard.js +19 -18
- package/cjs/interceptors/index.js +0 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +20 -3
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/cjs/modules/utils/utils.service.js +22 -103
- package/cjs/utils/error-handler.util.js +12 -67
- package/cjs/utils/html-sanitizer.util.js +1 -11
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-service.class.d.ts +2 -0
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/permissions.d.ts +12 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +0 -13
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/permissions.js +8 -1
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/response-payload.dto.js +0 -69
- package/fesm/guards/permission.guard.js +19 -18
- package/fesm/interceptors/index.js +0 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +3 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/fesm/modules/utils/utils.service.js +19 -89
- package/fesm/utils/error-handler.util.js +12 -68
- package/fesm/utils/html-sanitizer.util.js +1 -14
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +0 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +3 -0
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +2 -18
- package/package.json +2 -2
- package/utils/error-handler.util.d.ts +3 -18
- package/utils/html-sanitizer.util.d.ts +0 -1
- package/utils/index.d.ts +2 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -12
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -4
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
8
8
|
## Table of Contents
|
|
9
9
|
|
|
10
10
|
- [Overview](#overview)
|
|
11
|
-
- [Installation](#installation)
|
|
12
11
|
- [Package Architecture](#package-architecture)
|
|
13
12
|
- [ApiService - Generic CRUD Service](#apiservice---generic-crud-service)
|
|
13
|
+
- [RequestScopedApiService](#requestscopedapiservice)
|
|
14
14
|
- [ApiController - Generic CRUD Controller](#apicontroller---generic-crud-controller)
|
|
15
15
|
- [Decorators](#decorators)
|
|
16
16
|
- [Guards](#guards)
|
|
@@ -20,7 +20,9 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
20
20
|
- [Multi-Tenant DataSource](#multi-tenant-datasource)
|
|
21
21
|
- [DTOs](#dtos)
|
|
22
22
|
- [Base Entities](#base-entities)
|
|
23
|
+
- [Utilities](#utilities)
|
|
23
24
|
- [Error Handling](#error-handling)
|
|
25
|
+
- [Constants](#constants)
|
|
24
26
|
- [API Reference](#api-reference)
|
|
25
27
|
|
|
26
28
|
---
|
|
@@ -30,34 +32,26 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
30
32
|
`@flusys/nestjs-shared` provides shared utilities for building scalable NestJS applications:
|
|
31
33
|
|
|
32
34
|
- **Generic CRUD** - Standardized API controller and service patterns
|
|
33
|
-
- **Permission System** - Role and permission-based access control
|
|
34
|
-
- **Caching** - In-memory + Redis hybrid caching
|
|
35
|
+
- **Permission System** - Role and permission-based access control with complex logic
|
|
36
|
+
- **Caching** - In-memory + Redis hybrid caching (HybridCache)
|
|
35
37
|
- **Request Correlation** - AsyncLocalStorage-based request tracking
|
|
36
38
|
- **Middleware** - Logging, correlation, and performance monitoring
|
|
37
39
|
- **Interceptors** - Response metadata, idempotency, auto field setting
|
|
38
40
|
- **Multi-Tenancy** - Dynamic database connection management
|
|
39
|
-
- **Error Handling** - Centralized error handling
|
|
41
|
+
- **Error Handling** - Centralized error handling with sensitive data redaction
|
|
40
42
|
|
|
41
43
|
### Package Hierarchy
|
|
42
44
|
|
|
43
45
|
```
|
|
44
|
-
@flusys/nestjs-core
|
|
45
|
-
|
|
46
|
-
@flusys/nestjs-shared
|
|
47
|
-
|
|
48
|
-
@flusys/nestjs-auth
|
|
49
|
-
|
|
50
|
-
@flusys/nestjs-iam
|
|
51
|
-
|
|
52
|
-
@flusys/nestjs-storage
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Installation
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install @flusys/nestjs-shared @flusys/nestjs-core
|
|
46
|
+
@flusys/nestjs-core <- Pure TypeScript (foundation)
|
|
47
|
+
|
|
|
48
|
+
@flusys/nestjs-shared <- Shared NestJS utilities (THIS PACKAGE)
|
|
49
|
+
|
|
|
50
|
+
@flusys/nestjs-auth <- Uses common classes
|
|
51
|
+
|
|
|
52
|
+
@flusys/nestjs-iam <- Uses common patterns
|
|
53
|
+
|
|
|
54
|
+
@flusys/nestjs-storage <- Uses common patterns
|
|
61
55
|
```
|
|
62
56
|
|
|
63
57
|
---
|
|
@@ -73,17 +67,19 @@ nestjs-shared/
|
|
|
73
67
|
│ │ ├── request-scoped-api.service.ts # REQUEST-scoped service base
|
|
74
68
|
│ │ ├── hybrid-cache.class.ts # Two-tier caching
|
|
75
69
|
│ │ ├── winston.logger.class.ts # Winston logger config
|
|
76
|
-
│ │ ├── winston-logger-adapter.class.ts
|
|
77
|
-
│ │ └──
|
|
70
|
+
│ │ ├── winston-logger-adapter.class.ts
|
|
71
|
+
│ │ └── nest-logger-adapter.class.ts
|
|
78
72
|
│ │
|
|
79
73
|
│ ├── constants/ # Injection tokens & constants
|
|
74
|
+
│ │ ├── permissions.ts # Permission constants
|
|
80
75
|
│ │ └── index.ts
|
|
81
76
|
│ │
|
|
82
77
|
│ ├── decorators/ # Custom decorators
|
|
83
|
-
│ │ ├── api-response.decorator.ts #
|
|
84
|
-
│ │ ├── current-user.decorator.ts
|
|
85
|
-
│ │ ├── public.decorator.ts
|
|
86
|
-
│ │ ├── require-permission.decorator.ts
|
|
78
|
+
│ │ ├── api-response.decorator.ts # @ApiResponseDto
|
|
79
|
+
│ │ ├── current-user.decorator.ts # @CurrentUser
|
|
80
|
+
│ │ ├── public.decorator.ts # @Public
|
|
81
|
+
│ │ ├── require-permission.decorator.ts # @RequirePermission
|
|
82
|
+
│ │ ├── sanitize.decorator.ts # @SanitizeHtml, @SanitizeAndTrim
|
|
87
83
|
│ │ └── index.ts
|
|
88
84
|
│ │
|
|
89
85
|
│ ├── dtos/ # Shared DTOs
|
|
@@ -95,18 +91,16 @@ nestjs-shared/
|
|
|
95
91
|
│ │ └── index.ts
|
|
96
92
|
│ │
|
|
97
93
|
│ ├── entities/ # Base entities
|
|
98
|
-
│ │ ├── identity.ts
|
|
99
|
-
│ │ ├── user-root.ts
|
|
94
|
+
│ │ ├── identity.ts # Base entity with UUID
|
|
95
|
+
│ │ ├── user-root.ts # Base user entity
|
|
100
96
|
│ │ └── index.ts
|
|
101
97
|
│ │
|
|
102
98
|
│ ├── exceptions/ # Custom exceptions
|
|
103
|
-
│ │
|
|
104
|
-
│ │ └── index.ts
|
|
99
|
+
│ │ └── permission.exception.ts # Permission-related exceptions
|
|
105
100
|
│ │
|
|
106
101
|
│ ├── guards/ # Authentication & authorization
|
|
107
|
-
│ │ ├── jwt-auth.guard.ts
|
|
108
|
-
│ │
|
|
109
|
-
│ │ └── index.ts
|
|
102
|
+
│ │ ├── jwt-auth.guard.ts # JWT token validation
|
|
103
|
+
│ │ └── permission.guard.ts # Permission checks
|
|
110
104
|
│ │
|
|
111
105
|
│ ├── interceptors/ # Request/response interceptors
|
|
112
106
|
│ │ ├── delete-empty-id-from-body.interceptor.ts
|
|
@@ -116,35 +110,34 @@ nestjs-shared/
|
|
|
116
110
|
│ │ ├── set-create-by-on-body.interceptor.ts
|
|
117
111
|
│ │ ├── set-delete-by-on-body.interceptor.ts
|
|
118
112
|
│ │ ├── set-update-by-on-body.interceptor.ts
|
|
119
|
-
│ │
|
|
120
|
-
│ │ └── index.ts
|
|
113
|
+
│ │ └── slug.interceptor.ts
|
|
121
114
|
│ │
|
|
122
115
|
│ ├── interfaces/ # TypeScript interfaces
|
|
123
|
-
│ │ ├── api.interface.ts
|
|
116
|
+
│ │ ├── api.interface.ts # IService interface
|
|
124
117
|
│ │ ├── identity.interface.ts
|
|
125
|
-
│ │ ├── logged-user-info.interface.ts
|
|
126
|
-
│ │ ├── logger.interface.ts
|
|
127
|
-
│ │ ├── permission.interface.ts
|
|
128
|
-
│ │ └──
|
|
118
|
+
│ │ ├── logged-user-info.interface.ts # ILoggedUserInfo
|
|
119
|
+
│ │ ├── logger.interface.ts # ILogger
|
|
120
|
+
│ │ ├── permission.interface.ts # PermissionCondition
|
|
121
|
+
│ │ └── datasource-provider.interface.ts
|
|
129
122
|
│ │
|
|
130
123
|
│ ├── middlewares/ # Middleware
|
|
131
|
-
│ │
|
|
132
|
-
│ │ └── index.ts
|
|
124
|
+
│ │ └── logger.middleware.ts # Request logging & correlation
|
|
133
125
|
│ │
|
|
134
126
|
│ ├── modules/ # NestJS modules
|
|
135
127
|
│ │ ├── cache/cache.module.ts
|
|
136
128
|
│ │ ├── datasource/
|
|
137
129
|
│ │ │ ├── datasource.module.ts
|
|
138
|
-
│ │ │
|
|
139
|
-
│ │
|
|
140
|
-
│ │
|
|
141
|
-
│ │
|
|
142
|
-
│ │ │ └── utils.service.ts
|
|
143
|
-
│ │ └── index.ts
|
|
130
|
+
│ │ │ └── multi-tenant-datasource.service.ts
|
|
131
|
+
│ │ └── utils/
|
|
132
|
+
│ │ ├── utils.module.ts
|
|
133
|
+
│ │ └── utils.service.ts
|
|
144
134
|
│ │
|
|
145
135
|
│ └── utils/ # Utility functions
|
|
146
136
|
│ ├── error-handler.util.ts
|
|
147
|
-
│
|
|
137
|
+
│ ├── query-helpers.util.ts
|
|
138
|
+
│ ├── string.util.ts
|
|
139
|
+
│ ├── request.util.ts
|
|
140
|
+
│ └── html-sanitizer.util.ts
|
|
148
141
|
```
|
|
149
142
|
|
|
150
143
|
---
|
|
@@ -159,11 +152,7 @@ The `ApiService` base class provides standardized CRUD operations with caching,
|
|
|
159
152
|
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
160
153
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
161
154
|
import { Injectable, Inject } from '@nestjs/common';
|
|
162
|
-
import { InjectRepository } from '@nestjs/typeorm';
|
|
163
155
|
import { Repository } from 'typeorm';
|
|
164
|
-
import { User } from './user.entity';
|
|
165
|
-
import { CreateUserDto, UpdateUserDto } from './user.dto';
|
|
166
|
-
import { IUser } from './user.interface';
|
|
167
156
|
|
|
168
157
|
@Injectable()
|
|
169
158
|
export class UserService extends ApiService<
|
|
@@ -178,6 +167,7 @@ export class UserService extends ApiService<
|
|
|
178
167
|
protected override repository: Repository<User>,
|
|
179
168
|
@Inject('CACHE_INSTANCE')
|
|
180
169
|
protected override cacheManager: HybridCache,
|
|
170
|
+
@Inject(UtilsService)
|
|
181
171
|
protected override utilsService: UtilsService,
|
|
182
172
|
) {
|
|
183
173
|
super(
|
|
@@ -189,30 +179,6 @@ export class UserService extends ApiService<
|
|
|
189
179
|
true, // Enable caching
|
|
190
180
|
);
|
|
191
181
|
}
|
|
192
|
-
|
|
193
|
-
// Override to customize DTO to entity conversion
|
|
194
|
-
override async convertSingleDtoToEntity(
|
|
195
|
-
dto: CreateUserDto | UpdateUserDto,
|
|
196
|
-
user: ILoggedUserInfo,
|
|
197
|
-
): Promise<User> {
|
|
198
|
-
const entity = new User();
|
|
199
|
-
Object.assign(entity, dto);
|
|
200
|
-
return entity;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Override to customize query selection
|
|
204
|
-
override async getSelectQuery(
|
|
205
|
-
query: SelectQueryBuilder<User>,
|
|
206
|
-
user: ILoggedUserInfo,
|
|
207
|
-
select?: string[],
|
|
208
|
-
) {
|
|
209
|
-
if (!select?.length) {
|
|
210
|
-
select = ['id', 'name', 'email', 'createdAt'];
|
|
211
|
-
}
|
|
212
|
-
const selectFields = select.map(f => `${this.entityName}.${f}`);
|
|
213
|
-
query.select(selectFields);
|
|
214
|
-
return { query, isRaw: false };
|
|
215
|
-
}
|
|
216
182
|
}
|
|
217
183
|
```
|
|
218
184
|
|
|
@@ -224,11 +190,11 @@ export class UserService extends ApiService<
|
|
|
224
190
|
| `insertMany(dtos, user)` | Create multiple entities |
|
|
225
191
|
| `getById(id, user, select?)` | Get entity by ID |
|
|
226
192
|
| `findById(id, user, select?)` | Find entity (returns null if not found) |
|
|
193
|
+
| `findByIds(ids, user, select?)` | Find multiple by IDs |
|
|
227
194
|
| `getAll(dto, user)` | Get paginated list |
|
|
228
195
|
| `update(dto, user)` | Update single entity |
|
|
229
196
|
| `updateMany(dtos, user)` | Update multiple entities |
|
|
230
|
-
| `delete(dto, user)` | Soft/permanent delete |
|
|
231
|
-
| `restore(dto, user)` | Restore soft-deleted |
|
|
197
|
+
| `delete(dto, user)` | Soft/permanent delete or restore |
|
|
232
198
|
|
|
233
199
|
### Customization Hooks
|
|
234
200
|
|
|
@@ -244,6 +210,12 @@ export class UserService extends ApiService<...> {
|
|
|
244
210
|
// Add WHERE filters
|
|
245
211
|
override async getFilterQuery(query, filter, user): Promise<{ query, isRaw }> { }
|
|
246
212
|
|
|
213
|
+
// Add global search
|
|
214
|
+
override async getGlobalSearchQuery(query, globalSearch, user): Promise<{ query, isRaw }> { }
|
|
215
|
+
|
|
216
|
+
// Add sort order
|
|
217
|
+
override async getSortQuery(query, sort, user): Promise<{ query, isRaw }> { }
|
|
218
|
+
|
|
247
219
|
// Add extra query conditions (e.g., company filtering)
|
|
248
220
|
override async getExtraManipulateQuery(query, dto, user): Promise<{ query, isRaw }> { }
|
|
249
221
|
|
|
@@ -267,32 +239,61 @@ export class UserService extends ApiService<...> {
|
|
|
267
239
|
}
|
|
268
240
|
```
|
|
269
241
|
|
|
270
|
-
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## RequestScopedApiService
|
|
245
|
+
|
|
246
|
+
For dynamic entity resolution based on runtime configuration (e.g., company feature).
|
|
271
247
|
|
|
272
248
|
```typescript
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
249
|
+
import { RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
250
|
+
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
251
|
+
import { EntityTarget, Repository } from 'typeorm';
|
|
252
|
+
|
|
253
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
254
|
+
export class RoleService extends RequestScopedApiService<
|
|
255
|
+
CreateRoleDto,
|
|
256
|
+
UpdateRoleDto,
|
|
257
|
+
IRole,
|
|
258
|
+
RoleBase,
|
|
259
|
+
Repository<RoleBase>
|
|
260
|
+
> {
|
|
261
|
+
constructor(
|
|
262
|
+
@Inject('CACHE_INSTANCE') protected override cacheManager: HybridCache,
|
|
263
|
+
@Inject(UtilsService) protected override utilsService: UtilsService,
|
|
264
|
+
@Inject(ModuleConfigService) private readonly config: ModuleConfigService,
|
|
265
|
+
@Inject(DataSourceProvider) private readonly provider: DataSourceProvider,
|
|
266
|
+
) {
|
|
267
|
+
// Pass null for repository - will be initialized dynamically
|
|
268
|
+
super('role', null as any, cacheManager, utilsService, 'RoleService', true);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Required: Resolve which entity to use
|
|
272
|
+
protected resolveEntity(): EntityTarget<RoleBase> {
|
|
273
|
+
return this.config.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
283
274
|
}
|
|
284
275
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
branchId: user.branchId,
|
|
289
|
-
});
|
|
276
|
+
// Required: Return the DataSource provider
|
|
277
|
+
protected getDataSourceProvider(): IDataSourceProvider {
|
|
278
|
+
return this.provider;
|
|
290
279
|
}
|
|
291
280
|
|
|
292
|
-
|
|
281
|
+
// Optional: Initialize additional repositories
|
|
282
|
+
protected async initializeAdditionalRepositories(): Promise<void> {
|
|
283
|
+
this.actionRepository = await this.dataSourceProvider.getRepository(Action);
|
|
284
|
+
}
|
|
293
285
|
}
|
|
294
286
|
```
|
|
295
287
|
|
|
288
|
+
### Key Methods
|
|
289
|
+
|
|
290
|
+
| Method | Description |
|
|
291
|
+
|--------|-------------|
|
|
292
|
+
| `ensureRepositoryInitialized()` | Must call before using repository |
|
|
293
|
+
| `resolveEntity()` | Abstract - return entity class based on config |
|
|
294
|
+
| `getDataSourceProvider()` | Abstract - return datasource provider |
|
|
295
|
+
| `getDataSourceForService()` | Get raw DataSource for transactions |
|
|
296
|
+
|
|
296
297
|
---
|
|
297
298
|
|
|
298
299
|
## ApiController - Generic CRUD Controller
|
|
@@ -303,11 +304,8 @@ The `createApiController` factory creates standardized POST-only RPC controllers
|
|
|
303
304
|
|
|
304
305
|
```typescript
|
|
305
306
|
import { createApiController } from '@flusys/nestjs-shared/classes';
|
|
306
|
-
import { Controller } from '@nestjs/common';
|
|
307
|
+
import { Controller, Inject } from '@nestjs/common';
|
|
307
308
|
import { ApiTags } from '@nestjs/swagger';
|
|
308
|
-
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './user.dto';
|
|
309
|
-
import { IUser } from './user.interface';
|
|
310
|
-
import { UserService } from './user.service';
|
|
311
309
|
|
|
312
310
|
@ApiTags('Users')
|
|
313
311
|
@Controller('users')
|
|
@@ -318,7 +316,7 @@ export class UserController extends createApiController<
|
|
|
318
316
|
IUser,
|
|
319
317
|
UserService
|
|
320
318
|
>(CreateUserDto, UpdateUserDto, UserResponseDto) {
|
|
321
|
-
constructor(protected service: UserService) {
|
|
319
|
+
constructor(@Inject(UserService) protected service: UserService) {
|
|
322
320
|
super(service);
|
|
323
321
|
}
|
|
324
322
|
}
|
|
@@ -345,9 +343,7 @@ export class UserController extends createApiController(
|
|
|
345
343
|
CreateUserDto,
|
|
346
344
|
UpdateUserDto,
|
|
347
345
|
UserResponseDto,
|
|
348
|
-
{
|
|
349
|
-
security: 'jwt', // All endpoints require JWT
|
|
350
|
-
},
|
|
346
|
+
{ security: 'jwt' }, // All endpoints require JWT
|
|
351
347
|
) {}
|
|
352
348
|
|
|
353
349
|
// Per-endpoint security
|
|
@@ -366,29 +362,8 @@ export class UserController extends createApiController(
|
|
|
366
362
|
},
|
|
367
363
|
},
|
|
368
364
|
) {}
|
|
369
|
-
|
|
370
|
-
// Permission combinations
|
|
371
|
-
{
|
|
372
|
-
security: {
|
|
373
|
-
// Require ANY of these permissions
|
|
374
|
-
insert: {
|
|
375
|
-
level: 'permission',
|
|
376
|
-
permissions: ['users.create', 'users.admin'],
|
|
377
|
-
require: 'any', // Default
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
// Require ALL permissions
|
|
381
|
-
delete: {
|
|
382
|
-
level: 'permission',
|
|
383
|
-
permissions: ['users.delete', 'users.admin'],
|
|
384
|
-
require: 'all',
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
}
|
|
388
365
|
```
|
|
389
366
|
|
|
390
|
-
See [API-CONTROLLER-SECURITY.md](./API-CONTROLLER-SECURITY.md) for detailed security configuration.
|
|
391
|
-
|
|
392
367
|
---
|
|
393
368
|
|
|
394
369
|
## Decorators
|
|
@@ -403,16 +378,22 @@ import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
|
403
378
|
|
|
404
379
|
@Controller('profile')
|
|
405
380
|
export class ProfileController {
|
|
406
|
-
@
|
|
381
|
+
@Post('me')
|
|
407
382
|
getProfile(@CurrentUser() user: ILoggedUserInfo) {
|
|
408
383
|
return { userId: user.id, companyId: user.companyId };
|
|
409
384
|
}
|
|
385
|
+
|
|
386
|
+
// Extract specific property
|
|
387
|
+
@Post('id')
|
|
388
|
+
getUserId(@CurrentUser('id') userId: string) {
|
|
389
|
+
return { userId };
|
|
390
|
+
}
|
|
410
391
|
}
|
|
411
392
|
```
|
|
412
393
|
|
|
413
394
|
### @Public
|
|
414
395
|
|
|
415
|
-
Mark route as public (skip authentication)
|
|
396
|
+
Mark route as public (skip authentication). **Use sparingly - security risk**.
|
|
416
397
|
|
|
417
398
|
```typescript
|
|
418
399
|
import { Public } from '@flusys/nestjs-shared/decorators';
|
|
@@ -421,43 +402,44 @@ import { Public } from '@flusys/nestjs-shared/decorators';
|
|
|
421
402
|
export class AuthController {
|
|
422
403
|
@Public()
|
|
423
404
|
@Post('login')
|
|
424
|
-
login() {
|
|
425
|
-
// No JWT required
|
|
426
|
-
}
|
|
405
|
+
login() { }
|
|
427
406
|
}
|
|
428
407
|
```
|
|
429
408
|
|
|
430
409
|
### @RequirePermission
|
|
431
410
|
|
|
432
|
-
Require specific permission:
|
|
411
|
+
Require specific permission(s) - **AND logic** by default:
|
|
433
412
|
|
|
434
413
|
```typescript
|
|
435
414
|
import { RequirePermission } from '@flusys/nestjs-shared/decorators';
|
|
436
415
|
|
|
437
416
|
@Controller('admin')
|
|
438
417
|
export class AdminController {
|
|
418
|
+
// Requires 'admin.dashboard' permission
|
|
439
419
|
@RequirePermission('admin.dashboard')
|
|
440
|
-
@
|
|
441
|
-
getDashboard() {
|
|
442
|
-
|
|
443
|
-
|
|
420
|
+
@Post('dashboard')
|
|
421
|
+
getDashboard() { }
|
|
422
|
+
|
|
423
|
+
// Requires BOTH permissions
|
|
424
|
+
@RequirePermission('users.read', 'admin.access')
|
|
425
|
+
@Post('users')
|
|
426
|
+
getUsers() { }
|
|
444
427
|
}
|
|
445
428
|
```
|
|
446
429
|
|
|
447
430
|
### @RequireAnyPermission
|
|
448
431
|
|
|
449
|
-
Require any of the listed permissions
|
|
432
|
+
Require any of the listed permissions - **OR logic**:
|
|
450
433
|
|
|
451
434
|
```typescript
|
|
452
435
|
import { RequireAnyPermission } from '@flusys/nestjs-shared/decorators';
|
|
453
436
|
|
|
454
437
|
@Controller('reports')
|
|
455
438
|
export class ReportsController {
|
|
439
|
+
// Requires 'reports.view' OR 'reports.admin'
|
|
456
440
|
@RequireAnyPermission('reports.view', 'reports.admin')
|
|
457
|
-
@
|
|
458
|
-
getReports() {
|
|
459
|
-
// Requires 'reports.view' OR 'reports.admin'
|
|
460
|
-
}
|
|
441
|
+
@Post()
|
|
442
|
+
getReports() { }
|
|
461
443
|
}
|
|
462
444
|
```
|
|
463
445
|
|
|
@@ -470,14 +452,6 @@ import { RequirePermissionCondition } from '@flusys/nestjs-shared/decorators';
|
|
|
470
452
|
|
|
471
453
|
@Controller('sensitive')
|
|
472
454
|
export class SensitiveController {
|
|
473
|
-
// Simple: User needs 'admin' OR 'manager'
|
|
474
|
-
@RequirePermissionCondition({
|
|
475
|
-
operator: 'or',
|
|
476
|
-
permissions: ['admin', 'manager'],
|
|
477
|
-
})
|
|
478
|
-
@Get('simple')
|
|
479
|
-
getSimpleData() {}
|
|
480
|
-
|
|
481
455
|
// Complex: User needs 'users.read' AND ('admin' OR 'manager')
|
|
482
456
|
@RequirePermissionCondition({
|
|
483
457
|
operator: 'and',
|
|
@@ -486,33 +460,52 @@ export class SensitiveController {
|
|
|
486
460
|
{ operator: 'or', permissions: ['admin', 'manager'] }
|
|
487
461
|
]
|
|
488
462
|
})
|
|
489
|
-
@
|
|
490
|
-
getComplexData() {}
|
|
463
|
+
@Post('complex')
|
|
464
|
+
getComplexData() { }
|
|
465
|
+
}
|
|
466
|
+
```
|
|
491
467
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
@
|
|
501
|
-
|
|
468
|
+
### @SanitizeHtml / @SanitizeAndTrim
|
|
469
|
+
|
|
470
|
+
Escape HTML entities for XSS prevention:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { SanitizeHtml, SanitizeAndTrim } from '@flusys/nestjs-shared/decorators';
|
|
474
|
+
|
|
475
|
+
export class CreateCommentDto {
|
|
476
|
+
@SanitizeHtml()
|
|
477
|
+
@IsString()
|
|
478
|
+
content: string;
|
|
479
|
+
|
|
480
|
+
@SanitizeAndTrim() // Escapes HTML AND trims whitespace
|
|
481
|
+
@IsString()
|
|
482
|
+
title: string;
|
|
502
483
|
}
|
|
503
484
|
```
|
|
504
485
|
|
|
505
|
-
|
|
486
|
+
### @ApiResponseDto
|
|
487
|
+
|
|
488
|
+
Generates Swagger schema for response:
|
|
489
|
+
|
|
506
490
|
```typescript
|
|
507
|
-
|
|
491
|
+
import { ApiResponseDto } from '@flusys/nestjs-shared/decorators';
|
|
492
|
+
|
|
493
|
+
@Controller('users')
|
|
494
|
+
export class UserController {
|
|
495
|
+
@Post('get-all')
|
|
496
|
+
@ApiResponseDto(UserResponseDto, true, 'list') // Array with PaginationMetaDto
|
|
497
|
+
getAll() { }
|
|
498
|
+
|
|
499
|
+
@Post('insert')
|
|
500
|
+
@ApiResponseDto(UserResponseDto, false) // Single item
|
|
501
|
+
insert() { }
|
|
502
|
+
}
|
|
508
503
|
```
|
|
509
504
|
|
|
510
505
|
---
|
|
511
506
|
|
|
512
507
|
## Guards
|
|
513
508
|
|
|
514
|
-
The shared package provides two guards for authentication and authorization:
|
|
515
|
-
|
|
516
509
|
### JwtAuthGuard
|
|
517
510
|
|
|
518
511
|
Validates JWT tokens for protected routes. Extends Passport's `AuthGuard('jwt')` and respects `@Public()` decorator.
|
|
@@ -520,22 +513,15 @@ Validates JWT tokens for protected routes. Extends Passport's `AuthGuard('jwt')`
|
|
|
520
513
|
```typescript
|
|
521
514
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
522
515
|
|
|
523
|
-
// Apply globally
|
|
516
|
+
// Apply globally in main.ts
|
|
524
517
|
@Module({
|
|
525
518
|
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
|
|
526
519
|
})
|
|
527
520
|
export class AppModule {}
|
|
528
|
-
|
|
529
|
-
// Or per controller
|
|
530
|
-
@Controller('users')
|
|
531
|
-
@UseGuards(JwtAuthGuard)
|
|
532
|
-
export class UserController {
|
|
533
|
-
@Post('login')
|
|
534
|
-
@Public() // Skip JWT check
|
|
535
|
-
async login() { }
|
|
536
|
-
}
|
|
537
521
|
```
|
|
538
522
|
|
|
523
|
+
**Important:** Constructor needs `@Inject(Reflector)` for bundled code.
|
|
524
|
+
|
|
539
525
|
### PermissionGuard
|
|
540
526
|
|
|
541
527
|
Checks user permissions from cache with AND/OR/nested logic support.
|
|
@@ -543,9 +529,18 @@ Checks user permissions from cache with AND/OR/nested logic support.
|
|
|
543
529
|
```typescript
|
|
544
530
|
import { PermissionGuard } from '@flusys/nestjs-shared/guards';
|
|
545
531
|
|
|
546
|
-
// Apply globally
|
|
547
532
|
@Module({
|
|
548
|
-
providers: [
|
|
533
|
+
providers: [
|
|
534
|
+
{ provide: APP_GUARD, useClass: PermissionGuard },
|
|
535
|
+
{
|
|
536
|
+
provide: 'PERMISSION_GUARD_CONFIG',
|
|
537
|
+
useValue: {
|
|
538
|
+
enableCompanyFeature: true,
|
|
539
|
+
userPermissionKeyFormat: 'permissions:user:{userId}',
|
|
540
|
+
companyPermissionKeyFormat: 'permissions:company:{companyId}:branch:{branchId}:user:{userId}',
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
],
|
|
549
544
|
})
|
|
550
545
|
export class AppModule {}
|
|
551
546
|
```
|
|
@@ -560,11 +555,15 @@ export class AppModule {}
|
|
|
560
555
|
`permissions:company:{companyId}:branch:{branchId}:user:{userId}`
|
|
561
556
|
```
|
|
562
557
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
-
|
|
558
|
+
### Permission Exceptions
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import {
|
|
562
|
+
InsufficientPermissionsException, // 403 - Missing permissions
|
|
563
|
+
NoPermissionsFoundException, // 403 - No permissions in cache
|
|
564
|
+
PermissionSystemUnavailableException, // 500 - Cache unavailable
|
|
565
|
+
} from '@flusys/nestjs-shared/exceptions';
|
|
566
|
+
```
|
|
568
567
|
|
|
569
568
|
---
|
|
570
569
|
|
|
@@ -572,39 +571,24 @@ export class AppModule {}
|
|
|
572
571
|
|
|
573
572
|
### LoggerMiddleware
|
|
574
573
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
-
|
|
584
|
-
- **Tenant ID Tracking** - Extracts `x-tenant-id` from headers for multi-tenant apps
|
|
585
|
-
- **AsyncLocalStorage Context** - Thread-safe request context accessible anywhere
|
|
586
|
-
- **Security** - Automatically redacts sensitive headers (authorization, cookie, x-api-key)
|
|
587
|
-
- **Performance Monitoring** - Logs slow requests (>3s) with dedicated warning logs
|
|
588
|
-
- **Body Truncation** - Limits log size to 1000 characters
|
|
589
|
-
- **Debug Mode** - Conditionally logs headers and body based on LOG_LEVEL=debug
|
|
590
|
-
- **Complete Request Details** - URL, path, query params, content type, user agent, client IP
|
|
591
|
-
- **Complete Response Details** - Status code, message, content type, content length, user/company context
|
|
592
|
-
- **Multiple Response Hooks** - Captures responses via `res.send()`, `res.json()`, and `res.end()`
|
|
593
|
-
- **Error Handling** - Logs response errors with stack traces
|
|
594
|
-
- **User Context** - Automatically includes userId and companyId in response logs if available
|
|
574
|
+
Combined middleware for request correlation and HTTP logging.
|
|
575
|
+
|
|
576
|
+
**Features:**
|
|
577
|
+
- Request ID generation/tracking (UUID or from `x-request-id` header)
|
|
578
|
+
- Tenant ID tracking (from `x-tenant-id` header)
|
|
579
|
+
- AsyncLocalStorage context for thread-safe access
|
|
580
|
+
- Automatic sensitive header redaction (authorization, cookie, x-api-key)
|
|
581
|
+
- Performance monitoring (warns on requests > 3s)
|
|
582
|
+
- Body truncation (max 1000 chars)
|
|
595
583
|
|
|
596
584
|
**Usage:**
|
|
597
585
|
|
|
598
586
|
```typescript
|
|
599
587
|
import { LoggerMiddleware } from '@flusys/nestjs-shared/middlewares';
|
|
600
|
-
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
601
588
|
|
|
602
|
-
@Module({
|
|
603
|
-
// ...
|
|
604
|
-
})
|
|
589
|
+
@Module({})
|
|
605
590
|
export class AppModule implements NestModule {
|
|
606
591
|
configure(consumer: MiddlewareConsumer) {
|
|
607
|
-
// Apply to all routes
|
|
608
592
|
consumer.apply(LoggerMiddleware).forRoutes('*');
|
|
609
593
|
}
|
|
610
594
|
}
|
|
@@ -625,195 +609,28 @@ import {
|
|
|
625
609
|
@Injectable()
|
|
626
610
|
export class MyService {
|
|
627
611
|
async doSomething() {
|
|
628
|
-
const requestId = getRequestId();
|
|
629
|
-
const tenantId = getTenantId();
|
|
630
|
-
|
|
631
|
-
// Set user context after authentication
|
|
612
|
+
const requestId = getRequestId();
|
|
613
|
+
const tenantId = getTenantId();
|
|
632
614
|
setUserId('user-123');
|
|
633
|
-
setCompanyId('company-456');
|
|
634
|
-
|
|
635
|
-
// Use in logs
|
|
636
|
-
this.logger.info('Processing request', {
|
|
637
|
-
requestId,
|
|
638
|
-
tenantId,
|
|
639
|
-
userId: getUserId(),
|
|
640
|
-
companyId: getCompanyId(),
|
|
641
|
-
});
|
|
642
615
|
}
|
|
643
616
|
}
|
|
644
617
|
```
|
|
645
618
|
|
|
646
|
-
**Request Context Interface:**
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
interface IRequestContext {
|
|
650
|
-
requestId: string; // UUID or from x-request-id header
|
|
651
|
-
tenantId?: string; // From x-tenant-id header
|
|
652
|
-
userId?: string; // Set after authentication
|
|
653
|
-
companyId?: string; // Set after authentication
|
|
654
|
-
startTime: number; // Request start timestamp
|
|
655
|
-
}
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
**Configuration:**
|
|
659
|
-
|
|
660
|
-
```typescript
|
|
661
|
-
// Environment-based configuration
|
|
662
|
-
const IS_DEBUG = envConfig.getLogConfig().level === 'debug';
|
|
663
|
-
const TENANT_ID_HEADER = 'x-tenant-id';
|
|
664
|
-
const EXCLUDED_PATHS = ['/health', '/metrics', '/favicon.ico'];
|
|
665
|
-
const EXCLUDED_HEADERS = ['authorization', 'cookie', 'x-api-key'];
|
|
666
|
-
const MAX_BODY_LOG_SIZE = 1000;
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
**Console Log Format (Development):**
|
|
670
|
-
|
|
671
|
-
The development console logger displays HTTP requests in human-readable format:
|
|
672
|
-
|
|
673
|
-
```
|
|
674
|
-
2026-01-17 22:41:23 [INFO ] [HTTP] [POST /auth/login] [200] (45ms) [uuid] Incoming request
|
|
675
|
-
2026-01-17 22:41:23 [INFO ] [HTTP] [POST /auth/login] [200] (45ms) [uuid] (user:user-123) Response [200]
|
|
676
|
-
2026-01-17 22:41:25 [WARN ] [HTTP] [GET /api/users] [404] (12ms) [uuid] Response [404]
|
|
677
|
-
2026-01-17 22:41:30 [ERROR ] [HTTP] [POST /api/orders] [500] (234ms) [uuid] (user:user-123) Response [500]
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
**Format Structure:**
|
|
681
|
-
- `[timestamp]` - Request timestamp
|
|
682
|
-
- `[level]` - Log level (INFO, WARN, ERROR)
|
|
683
|
-
- `[context]` - Always "HTTP" for HTTP requests
|
|
684
|
-
- `[METHOD /path]` - HTTP method and endpoint path
|
|
685
|
-
- `[statusCode]` - HTTP response status (only in response logs)
|
|
686
|
-
- `(duration)` - Request duration (only in response logs)
|
|
687
|
-
- `[uuid]` - Request correlation ID
|
|
688
|
-
- `(user:userId)` - User ID if authenticated (only in response logs)
|
|
689
|
-
|
|
690
|
-
**File Log Format (Production):**
|
|
691
|
-
|
|
692
|
-
```json
|
|
693
|
-
// Incoming request (Enhanced with full details)
|
|
694
|
-
{
|
|
695
|
-
"level": "info",
|
|
696
|
-
"message": "Incoming request",
|
|
697
|
-
"context": "HTTP",
|
|
698
|
-
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
699
|
-
"tenantId": "tenant-123",
|
|
700
|
-
"method": "POST",
|
|
701
|
-
"url": "/api/users/insert?sort=name",
|
|
702
|
-
"path": "/api/users/insert",
|
|
703
|
-
"query": { "sort": "name" },
|
|
704
|
-
"ip": "192.168.1.100",
|
|
705
|
-
"userAgent": "Mozilla/5.0...",
|
|
706
|
-
"contentType": "application/json",
|
|
707
|
-
"contentLength": "342",
|
|
708
|
-
"headers": { ... }, // Only in debug mode
|
|
709
|
-
"body": { ... }, // Only in debug mode, truncated to 1000 chars
|
|
710
|
-
"params": { ... } // Only in debug mode (route params)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Response (Enhanced with full details)
|
|
714
|
-
{
|
|
715
|
-
"level": "info",
|
|
716
|
-
"message": "Response [200]",
|
|
717
|
-
"context": "HTTP",
|
|
718
|
-
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
719
|
-
"tenantId": "tenant-123",
|
|
720
|
-
"method": "POST",
|
|
721
|
-
"url": "/api/users/insert?sort=name",
|
|
722
|
-
"path": "/api/users/insert",
|
|
723
|
-
"statusCode": 200,
|
|
724
|
-
"statusMessage": "OK",
|
|
725
|
-
"duration": "125ms",
|
|
726
|
-
"durationMs": 125,
|
|
727
|
-
"contentType": "application/json; charset=utf-8",
|
|
728
|
-
"contentLength": "156",
|
|
729
|
-
"userId": "user-456", // Automatically added if available
|
|
730
|
-
"companyId": "company-789" // Automatically added if available
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Error response (includes response body automatically)
|
|
734
|
-
{
|
|
735
|
-
"level": "warn",
|
|
736
|
-
"message": "Response [400]",
|
|
737
|
-
"context": "HTTP",
|
|
738
|
-
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
739
|
-
"tenantId": "tenant-123",
|
|
740
|
-
"method": "POST",
|
|
741
|
-
"url": "/api/users/insert",
|
|
742
|
-
"path": "/api/users/insert",
|
|
743
|
-
"statusCode": 400,
|
|
744
|
-
"statusMessage": "Bad Request",
|
|
745
|
-
"duration": "45ms",
|
|
746
|
-
"durationMs": 45,
|
|
747
|
-
"userId": "user-456",
|
|
748
|
-
"companyId": "company-789",
|
|
749
|
-
"responseBody": {
|
|
750
|
-
"statusCode": 400,
|
|
751
|
-
"message": "Validation failed",
|
|
752
|
-
"errors": ["Email is required"]
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Slow request warning (Enhanced with more context)
|
|
757
|
-
{
|
|
758
|
-
"level": "warn",
|
|
759
|
-
"message": "Slow request detected",
|
|
760
|
-
"context": "HTTP",
|
|
761
|
-
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
762
|
-
"tenantId": "tenant-123",
|
|
763
|
-
"userId": "user-456",
|
|
764
|
-
"companyId": "company-789",
|
|
765
|
-
"method": "POST",
|
|
766
|
-
"url": "/api/reports/generate",
|
|
767
|
-
"path": "/api/reports/generate",
|
|
768
|
-
"duration": "3245ms",
|
|
769
|
-
"durationMs": 3245,
|
|
770
|
-
"threshold": "3000ms"
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Response error
|
|
774
|
-
{
|
|
775
|
-
"level": "error",
|
|
776
|
-
"message": "Response error",
|
|
777
|
-
"context": "HTTP",
|
|
778
|
-
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
779
|
-
"tenantId": "tenant-123",
|
|
780
|
-
"method": "POST",
|
|
781
|
-
"url": "/api/users/insert",
|
|
782
|
-
"path": "/api/users/insert",
|
|
783
|
-
"error": "Socket hang up",
|
|
784
|
-
"stack": "Error: Socket hang up\n at ..."
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
**Benefits:**
|
|
789
|
-
|
|
790
|
-
- **Request Tracing** - Correlate logs across multiple services using requestId
|
|
791
|
-
- **Multi-Tenant Support** - Automatic tenant isolation in logs
|
|
792
|
-
- **Security** - Sensitive data automatically redacted
|
|
793
|
-
- **Performance Monitoring** - Identify slow endpoints
|
|
794
|
-
- **Debugging** - Conditional verbose logging
|
|
795
|
-
|
|
796
619
|
---
|
|
797
620
|
|
|
798
621
|
## Interceptors
|
|
799
622
|
|
|
800
623
|
### ResponseMetaInterceptor
|
|
801
624
|
|
|
802
|
-
Adds
|
|
625
|
+
Adds `_meta` to all responses:
|
|
803
626
|
|
|
804
627
|
```typescript
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
@UseInterceptors(ResponseMetaInterceptor)
|
|
808
|
-
@Controller('users')
|
|
809
|
-
export class UserController {}
|
|
810
|
-
|
|
811
|
-
// Response:
|
|
628
|
+
// Response includes:
|
|
812
629
|
{
|
|
813
630
|
"data": [...],
|
|
814
|
-
"
|
|
815
|
-
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
631
|
+
"_meta": {
|
|
816
632
|
"requestId": "abc-123",
|
|
633
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
817
634
|
"responseTime": 45
|
|
818
635
|
}
|
|
819
636
|
}
|
|
@@ -821,63 +638,23 @@ export class UserController {}
|
|
|
821
638
|
|
|
822
639
|
### IdempotencyInterceptor
|
|
823
640
|
|
|
824
|
-
|
|
641
|
+
Prevents duplicate POST requests using `X-Idempotency-Key` header. Caches responses for 24 hours.
|
|
825
642
|
|
|
826
|
-
|
|
827
|
-
import { IdempotencyInterceptor } from '@flusys/nestjs-shared/interceptors';
|
|
643
|
+
### SetCreatedByOnBody / SetUpdateByOnBody / SetDeletedByOnBody
|
|
828
644
|
|
|
829
|
-
|
|
830
|
-
@Controller('payments')
|
|
831
|
-
export class PaymentController {
|
|
832
|
-
@Post()
|
|
833
|
-
// Clients send X-Idempotency-Key header
|
|
834
|
-
// Duplicate requests return cached response
|
|
835
|
-
processPayment() {}
|
|
836
|
-
}
|
|
837
|
-
```
|
|
645
|
+
Auto-set audit user IDs on request body from authenticated user.
|
|
838
646
|
|
|
839
|
-
###
|
|
647
|
+
### DeleteEmptyIdFromBodyInterceptor
|
|
840
648
|
|
|
841
|
-
|
|
649
|
+
Removes empty `id` fields from request body (single and array bodies).
|
|
842
650
|
|
|
843
|
-
|
|
844
|
-
import { SetCreatedByOnBody, SetUpdateByOnBody } from '@flusys/nestjs-shared/interceptors';
|
|
845
|
-
|
|
846
|
-
@Controller('posts')
|
|
847
|
-
export class PostController {
|
|
848
|
-
@UseInterceptors(SetCreatedByOnBody)
|
|
849
|
-
@Post()
|
|
850
|
-
create(@Body() dto: CreatePostDto) {
|
|
851
|
-
// dto.createdById is automatically set to current user ID
|
|
852
|
-
}
|
|
651
|
+
### QueryPerformanceInterceptor
|
|
853
652
|
|
|
854
|
-
|
|
855
|
-
@Put()
|
|
856
|
-
update(@Body() dto: UpdatePostDto) {
|
|
857
|
-
// dto.updatedById is automatically set to current user ID
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
```
|
|
653
|
+
Monitors execution time and warns if request > 1000ms (configurable).
|
|
861
654
|
|
|
862
655
|
### Slug Interceptor
|
|
863
656
|
|
|
864
|
-
Auto-
|
|
865
|
-
|
|
866
|
-
```typescript
|
|
867
|
-
import { Slug } from '@flusys/nestjs-shared/interceptors';
|
|
868
|
-
|
|
869
|
-
@Controller('products')
|
|
870
|
-
export class ProductController {
|
|
871
|
-
@UseInterceptors(Slug)
|
|
872
|
-
@Post()
|
|
873
|
-
create(@Body() dto: CreateProductDto) {
|
|
874
|
-
// dto.slug is auto-generated from dto.name
|
|
875
|
-
// "My Product" -> "my-product"
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
**Note:** Requires `UtilsModule` to be imported in your module for dependency injection.
|
|
657
|
+
Auto-generates `slug` from `name` field using `UtilsService.transformToSlug()`.
|
|
881
658
|
|
|
882
659
|
---
|
|
883
660
|
|
|
@@ -885,29 +662,22 @@ export class ProductController {
|
|
|
885
662
|
|
|
886
663
|
### HybridCache
|
|
887
664
|
|
|
888
|
-
Two-tier caching with in-memory (
|
|
665
|
+
Two-tier caching with in-memory (L1) and Redis (L2):
|
|
889
666
|
|
|
890
667
|
```typescript
|
|
891
668
|
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
892
669
|
|
|
893
670
|
@Injectable()
|
|
894
671
|
export class MyService {
|
|
895
|
-
constructor(
|
|
896
|
-
@Inject('CACHE_INSTANCE')
|
|
897
|
-
private cache: HybridCache,
|
|
898
|
-
) {}
|
|
672
|
+
constructor(@Inject('CACHE_INSTANCE') private cache: HybridCache) {}
|
|
899
673
|
|
|
900
674
|
async getData(key: string) {
|
|
901
675
|
// Check cache first
|
|
902
676
|
const cached = await this.cache.get(key);
|
|
903
677
|
if (cached) return cached;
|
|
904
678
|
|
|
905
|
-
// Fetch from database
|
|
906
679
|
const data = await this.fetchFromDb();
|
|
907
|
-
|
|
908
|
-
// Store in cache (TTL in seconds)
|
|
909
|
-
await this.cache.set(key, data, 3600);
|
|
910
|
-
|
|
680
|
+
await this.cache.set(key, data, 3600); // TTL in seconds
|
|
911
681
|
return data;
|
|
912
682
|
}
|
|
913
683
|
|
|
@@ -915,9 +685,9 @@ export class MyService {
|
|
|
915
685
|
await this.cache.del(key);
|
|
916
686
|
}
|
|
917
687
|
|
|
918
|
-
async
|
|
919
|
-
//
|
|
920
|
-
await this.cache.
|
|
688
|
+
async invalidateAll() {
|
|
689
|
+
await this.cache.reset(); // Clear L1
|
|
690
|
+
await this.cache.resetL2(); // Clear L2 (Redis)
|
|
921
691
|
}
|
|
922
692
|
}
|
|
923
693
|
```
|
|
@@ -929,40 +699,26 @@ import { CacheModule } from '@flusys/nestjs-shared/modules';
|
|
|
929
699
|
|
|
930
700
|
@Module({
|
|
931
701
|
imports: [
|
|
932
|
-
CacheModule.forRoot(
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
},
|
|
938
|
-
|
|
939
|
-
// Redis config (optional)
|
|
940
|
-
redis: {
|
|
941
|
-
url: 'redis://localhost:6379',
|
|
942
|
-
ttl: 3600,
|
|
943
|
-
},
|
|
944
|
-
}),
|
|
702
|
+
CacheModule.forRoot(
|
|
703
|
+
true, // isGlobal
|
|
704
|
+
60_000, // memoryTtl (ms)
|
|
705
|
+
5000 // memorySize (LRU max items)
|
|
706
|
+
),
|
|
945
707
|
],
|
|
946
708
|
})
|
|
947
709
|
export class AppModule {}
|
|
948
710
|
```
|
|
949
711
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
| `get(key)` | Get cached value |
|
|
955
|
-
| `set(key, value, ttl?)` | Set value with optional TTL |
|
|
956
|
-
| `del(key)` | Delete single key |
|
|
957
|
-
| `delByPattern(pattern)` | Delete keys matching pattern |
|
|
958
|
-
| `reset()` | Clear all cache |
|
|
959
|
-
| `has(key)` | Check if key exists |
|
|
712
|
+
**Configuration via `USE_CACHE_LABEL` env:**
|
|
713
|
+
- `'memory'` - L1 only
|
|
714
|
+
- `'redis'` - L2 only
|
|
715
|
+
- `'hybrid'` - Both (default)
|
|
960
716
|
|
|
961
717
|
---
|
|
962
718
|
|
|
963
719
|
## Multi-Tenant DataSource
|
|
964
720
|
|
|
965
|
-
Dynamic database connection management with connection pooling
|
|
721
|
+
Dynamic database connection management with connection pooling.
|
|
966
722
|
|
|
967
723
|
### Setup
|
|
968
724
|
|
|
@@ -982,7 +738,7 @@ import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
|
982
738
|
},
|
|
983
739
|
tenants: [
|
|
984
740
|
{ id: 'tenant1', database: 'tenant1_db' },
|
|
985
|
-
{ id: 'tenant2', database: 'tenant2_db' },
|
|
741
|
+
{ id: 'tenant2', database: 'tenant2_db', host: 'other-server.com' },
|
|
986
742
|
],
|
|
987
743
|
}),
|
|
988
744
|
],
|
|
@@ -990,38 +746,18 @@ import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
|
990
746
|
export class AppModule {}
|
|
991
747
|
```
|
|
992
748
|
|
|
993
|
-
###
|
|
749
|
+
### MultiTenantDataSourceService
|
|
994
750
|
|
|
995
751
|
| Method | Description |
|
|
996
752
|
|--------|-------------|
|
|
997
753
|
| `getDataSource()` | Get DataSource for current tenant (from header) |
|
|
998
754
|
| `getDataSourceForTenant(id)` | Get DataSource for specific tenant |
|
|
999
755
|
| `getRepository(entity)` | Get repository for current tenant |
|
|
1000
|
-
| `withTenant(id, callback)` | Execute callback with specific tenant
|
|
1001
|
-
| `forAllTenants(callback)` | Execute callback for all
|
|
756
|
+
| `withTenant(id, callback)` | Execute callback with specific tenant |
|
|
757
|
+
| `forAllTenants(callback)` | Execute callback for all tenants |
|
|
1002
758
|
| `registerTenant(config)` | Register new tenant at runtime |
|
|
1003
759
|
| `removeTenant(id)` | Remove tenant and close connection |
|
|
1004
|
-
|
|
1005
|
-
### Usage
|
|
1006
|
-
|
|
1007
|
-
```typescript
|
|
1008
|
-
@Injectable()
|
|
1009
|
-
export class TenantService {
|
|
1010
|
-
constructor(private dataSource: MultiTenantDataSourceService) {}
|
|
1011
|
-
|
|
1012
|
-
async getUsers() {
|
|
1013
|
-
// Auto-resolves tenant from X-Tenant-ID header
|
|
1014
|
-
const repo = await this.dataSource.getRepository(User);
|
|
1015
|
-
return repo.find();
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
async crossTenantCopy(fromId: string, toId: string) {
|
|
1019
|
-
const fromDs = await this.dataSource.getDataSourceForTenant(fromId);
|
|
1020
|
-
const toDs = await this.dataSource.getDataSourceForTenant(toId);
|
|
1021
|
-
// Copy data between tenants
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
```
|
|
760
|
+
| `getCurrentTenantId()` | Get tenant ID from request header |
|
|
1025
761
|
|
|
1026
762
|
---
|
|
1027
763
|
|
|
@@ -1029,89 +765,71 @@ export class TenantService {
|
|
|
1029
765
|
|
|
1030
766
|
### FilterAndPaginationDto
|
|
1031
767
|
|
|
1032
|
-
Standard filtering and pagination:
|
|
1033
|
-
|
|
1034
768
|
```typescript
|
|
1035
769
|
import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
|
|
1036
770
|
|
|
1037
771
|
// Request body
|
|
1038
772
|
{
|
|
1039
|
-
"filter": {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
"pagination": {
|
|
1044
|
-
"currentPage": 0,
|
|
1045
|
-
"pageSize": 10
|
|
1046
|
-
},
|
|
1047
|
-
"sort": {
|
|
1048
|
-
"createdAt": "DESC",
|
|
1049
|
-
"name": "ASC"
|
|
1050
|
-
},
|
|
1051
|
-
"select": ["id", "name", "price"],
|
|
773
|
+
"filter": { "status": "active" },
|
|
774
|
+
"pagination": { "currentPage": 0, "pageSize": 10 },
|
|
775
|
+
"sort": { "createdAt": "DESC" },
|
|
776
|
+
"select": ["id", "name", "email"],
|
|
1052
777
|
"withDeleted": false
|
|
1053
778
|
}
|
|
1054
779
|
```
|
|
1055
780
|
|
|
1056
781
|
### DeleteDto
|
|
1057
782
|
|
|
1058
|
-
Soft or permanent delete:
|
|
1059
|
-
|
|
1060
783
|
```typescript
|
|
1061
784
|
import { DeleteDto } from '@flusys/nestjs-shared/dtos';
|
|
1062
785
|
|
|
1063
|
-
// Soft delete
|
|
1064
|
-
{
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
786
|
+
// Soft delete
|
|
787
|
+
{ "id": "uuid", "type": "delete" }
|
|
788
|
+
|
|
789
|
+
// Restore
|
|
790
|
+
{ "id": "uuid", "type": "restore" }
|
|
1068
791
|
|
|
1069
792
|
// Permanent delete
|
|
1070
|
-
{
|
|
1071
|
-
"id": "user-123",
|
|
1072
|
-
"type": "permanent"
|
|
1073
|
-
}
|
|
793
|
+
{ "id": "uuid", "type": "permanent" }
|
|
1074
794
|
|
|
1075
795
|
// Multiple IDs
|
|
1076
|
-
{
|
|
1077
|
-
"id": ["user-123", "user-456"],
|
|
1078
|
-
"type": "soft"
|
|
1079
|
-
}
|
|
796
|
+
{ "id": ["uuid1", "uuid2"], "type": "delete" }
|
|
1080
797
|
```
|
|
1081
798
|
|
|
1082
|
-
###
|
|
1083
|
-
|
|
1084
|
-
Standardized response format:
|
|
799
|
+
### Response DTOs
|
|
1085
800
|
|
|
1086
801
|
```typescript
|
|
1087
|
-
|
|
802
|
+
// Single item
|
|
803
|
+
class SingleResponseDto<T> {
|
|
804
|
+
success: boolean;
|
|
805
|
+
message: string;
|
|
806
|
+
data?: T;
|
|
807
|
+
_meta?: RequestMetaDto;
|
|
808
|
+
}
|
|
1088
809
|
|
|
1089
|
-
//
|
|
1090
|
-
{
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
810
|
+
// Paginated list
|
|
811
|
+
class ListResponseDto<T> {
|
|
812
|
+
success: boolean;
|
|
813
|
+
message: string;
|
|
814
|
+
data?: T[];
|
|
815
|
+
meta: PaginationMetaDto; // { total, page, pageSize, count, hasMore?, totalPages? }
|
|
816
|
+
_meta?: RequestMetaDto;
|
|
1094
817
|
}
|
|
1095
818
|
|
|
1096
|
-
//
|
|
1097
|
-
{
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
"limit": 10,
|
|
1104
|
-
"totalPages": 10
|
|
1105
|
-
}
|
|
819
|
+
// Bulk operations
|
|
820
|
+
class BulkResponseDto<T> {
|
|
821
|
+
success: boolean;
|
|
822
|
+
message: string;
|
|
823
|
+
data?: T[];
|
|
824
|
+
meta: BulkMetaDto; // { count, failed?, total? }
|
|
825
|
+
_meta?: RequestMetaDto;
|
|
1106
826
|
}
|
|
1107
827
|
|
|
1108
|
-
//
|
|
1109
|
-
{
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
"message": "Invalid input"
|
|
1114
|
-
}
|
|
828
|
+
// Message only
|
|
829
|
+
class MessageResponseDto {
|
|
830
|
+
success: boolean;
|
|
831
|
+
message: string;
|
|
832
|
+
_meta?: RequestMetaDto;
|
|
1115
833
|
}
|
|
1116
834
|
```
|
|
1117
835
|
|
|
@@ -1129,65 +847,174 @@ import { Identity } from '@flusys/nestjs-shared/entities';
|
|
|
1129
847
|
@Entity()
|
|
1130
848
|
export class Product extends Identity {
|
|
1131
849
|
// Inherited: id, createdAt, updatedAt, deletedAt
|
|
850
|
+
// Inherited: createdById, updatedById, deletedById
|
|
1132
851
|
|
|
1133
852
|
@Column()
|
|
1134
853
|
name: string;
|
|
1135
|
-
|
|
1136
|
-
@Column()
|
|
1137
|
-
price: number;
|
|
1138
854
|
}
|
|
1139
855
|
```
|
|
1140
856
|
|
|
1141
857
|
### UserRoot Entity
|
|
1142
858
|
|
|
1143
|
-
Base user entity:
|
|
859
|
+
Base user entity with common fields:
|
|
1144
860
|
|
|
1145
861
|
```typescript
|
|
1146
862
|
import { UserRoot } from '@flusys/nestjs-shared/entities';
|
|
1147
863
|
|
|
1148
864
|
@Entity()
|
|
1149
865
|
export class User extends UserRoot {
|
|
1150
|
-
// Inherited: id, name, email,
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
866
|
+
// Inherited: id, name, email, phone, profilePictureId
|
|
867
|
+
// Inherited: isActive, emailVerified, phoneVerified
|
|
868
|
+
// Inherited: lastLoginAt, additionalFields
|
|
869
|
+
// Inherited: createdAt, updatedAt, deletedAt
|
|
1154
870
|
}
|
|
1155
871
|
```
|
|
1156
872
|
|
|
1157
873
|
---
|
|
1158
874
|
|
|
1159
|
-
##
|
|
875
|
+
## Utilities
|
|
876
|
+
|
|
877
|
+
### Query Helpers
|
|
1160
878
|
|
|
1161
|
-
|
|
879
|
+
```typescript
|
|
880
|
+
import {
|
|
881
|
+
applyCompanyFilter,
|
|
882
|
+
buildCompanyWhereCondition,
|
|
883
|
+
hasCompanyId,
|
|
884
|
+
validateCompanyOwnership,
|
|
885
|
+
} from '@flusys/nestjs-shared/utils';
|
|
1162
886
|
|
|
1163
|
-
|
|
887
|
+
// Add company filter to TypeORM query
|
|
888
|
+
applyCompanyFilter(query, {
|
|
889
|
+
isCompanyFeatureEnabled: true,
|
|
890
|
+
entityAlias: 'entity',
|
|
891
|
+
}, user);
|
|
892
|
+
|
|
893
|
+
// Build where condition for company
|
|
894
|
+
const where = buildCompanyWhereCondition(baseWhere, user, isCompanyFeatureEnabled);
|
|
895
|
+
|
|
896
|
+
// Validate entity belongs to user's company
|
|
897
|
+
validateCompanyOwnership(entity, user, 'Entity');
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### String Utilities
|
|
1164
901
|
|
|
1165
902
|
```typescript
|
|
1166
|
-
import {
|
|
903
|
+
import { generateSlug, generateUniqueSlug } from '@flusys/nestjs-shared/utils';
|
|
904
|
+
|
|
905
|
+
// Generate URL-friendly slug
|
|
906
|
+
const slug = generateSlug('My Product Name', 100);
|
|
907
|
+
// Returns: 'my-product-name'
|
|
908
|
+
|
|
909
|
+
// Generate unique slug with collision detection
|
|
910
|
+
const uniqueSlug = await generateUniqueSlug(
|
|
911
|
+
'My Product',
|
|
912
|
+
async (pattern) => existingSlugs.filter(s => s.startsWith(pattern)),
|
|
913
|
+
100
|
|
914
|
+
);
|
|
915
|
+
```
|
|
1167
916
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
917
|
+
### Request Utilities
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
import {
|
|
921
|
+
isBrowserRequest,
|
|
922
|
+
buildCookieOptions,
|
|
923
|
+
parseDurationToMs,
|
|
924
|
+
} from '@flusys/nestjs-shared/utils';
|
|
925
|
+
|
|
926
|
+
// Detect browser vs API client
|
|
927
|
+
const isBrowser = isBrowserRequest(req);
|
|
928
|
+
|
|
929
|
+
// Build secure cookie options
|
|
930
|
+
const cookieOpts = buildCookieOptions(req);
|
|
931
|
+
|
|
932
|
+
// Parse duration string
|
|
933
|
+
const ms = parseDurationToMs('7d'); // 604800000
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### HTML Sanitizer
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
import { escapeHtml, escapeHtmlVariables } from '@flusys/nestjs-shared/utils';
|
|
940
|
+
|
|
941
|
+
// Escape single string
|
|
942
|
+
const safe = escapeHtml('<script>alert("xss")</script>');
|
|
943
|
+
// Returns: '<script>alert("xss")</script>'
|
|
944
|
+
|
|
945
|
+
// Escape all values in an object
|
|
946
|
+
const safeVars = escapeHtmlVariables({
|
|
947
|
+
userName: '<script>evil</script>',
|
|
948
|
+
message: 'Hello, World!',
|
|
949
|
+
});
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## Error Handling
|
|
955
|
+
|
|
956
|
+
### Error Handler Utilities
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
import {
|
|
960
|
+
getErrorMessage,
|
|
961
|
+
logError,
|
|
962
|
+
rethrowError,
|
|
963
|
+
logAndRethrow,
|
|
964
|
+
} from '@flusys/nestjs-shared/utils';
|
|
965
|
+
|
|
966
|
+
interface IErrorContext {
|
|
967
|
+
operation?: string;
|
|
968
|
+
entity?: string;
|
|
969
|
+
userId?: string;
|
|
970
|
+
id?: string;
|
|
971
|
+
companyId?: string;
|
|
972
|
+
data?: Record<string, unknown>;
|
|
1178
973
|
}
|
|
974
|
+
|
|
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 });
|
|
980
|
+
|
|
981
|
+
// Type-safe rethrow
|
|
982
|
+
rethrowError(error);
|
|
983
|
+
|
|
984
|
+
// Combined log + rethrow
|
|
985
|
+
logAndRethrow(logger, error, 'updateUser', context);
|
|
1179
986
|
```
|
|
1180
987
|
|
|
1181
|
-
|
|
988
|
+
---
|
|
989
|
+
|
|
990
|
+
## Constants
|
|
991
|
+
|
|
992
|
+
### Permission Constants
|
|
1182
993
|
|
|
1183
994
|
```typescript
|
|
1184
|
-
|
|
995
|
+
import { PERMISSIONS } from '@flusys/nestjs-shared/constants';
|
|
996
|
+
|
|
997
|
+
// Organized by module:
|
|
998
|
+
PERMISSIONS.USER.CREATE // 'user.create'
|
|
999
|
+
PERMISSIONS.USER.READ // 'user.read'
|
|
1000
|
+
PERMISSIONS.USER.UPDATE // 'user.update'
|
|
1001
|
+
PERMISSIONS.USER.DELETE // 'user.delete'
|
|
1002
|
+
|
|
1003
|
+
PERMISSIONS.COMPANY.CREATE // 'company.create'
|
|
1004
|
+
PERMISSIONS.BRANCH.CREATE // 'branch.create'
|
|
1005
|
+
|
|
1006
|
+
PERMISSIONS.ROLE.CREATE // 'role.create'
|
|
1007
|
+
PERMISSIONS.FILE.CREATE // 'file.create'
|
|
1008
|
+
PERMISSIONS.FOLDER.CREATE // 'folder.create'
|
|
1009
|
+
// ...etc
|
|
1010
|
+
```
|
|
1185
1011
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1012
|
+
### Injection Tokens
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
'CACHE_INSTANCE' // HybridCache provider
|
|
1016
|
+
'PERMISSION_GUARD_CONFIG' // PermissionGuard config
|
|
1017
|
+
'LOGGER_INSTANCE' // Logger provider
|
|
1191
1018
|
```
|
|
1192
1019
|
|
|
1193
1020
|
---
|
|
@@ -1200,8 +1027,11 @@ export class UserService {
|
|
|
1200
1027
|
// Classes
|
|
1201
1028
|
import {
|
|
1202
1029
|
ApiService,
|
|
1030
|
+
RequestScopedApiService,
|
|
1203
1031
|
createApiController,
|
|
1204
1032
|
HybridCache,
|
|
1033
|
+
WinstonLoggerAdapter,
|
|
1034
|
+
NestLoggerAdapter,
|
|
1205
1035
|
} from '@flusys/nestjs-shared/classes';
|
|
1206
1036
|
|
|
1207
1037
|
// Decorators
|
|
@@ -1211,13 +1041,13 @@ import {
|
|
|
1211
1041
|
RequirePermission,
|
|
1212
1042
|
RequireAnyPermission,
|
|
1213
1043
|
RequirePermissionCondition,
|
|
1044
|
+
SanitizeHtml,
|
|
1045
|
+
SanitizeAndTrim,
|
|
1046
|
+
ApiResponseDto,
|
|
1214
1047
|
} from '@flusys/nestjs-shared/decorators';
|
|
1215
1048
|
|
|
1216
1049
|
// Guards
|
|
1217
|
-
import {
|
|
1218
|
-
JwtAuthGuard,
|
|
1219
|
-
PermissionGuard,
|
|
1220
|
-
} from '@flusys/nestjs-shared/guards';
|
|
1050
|
+
import { JwtAuthGuard, PermissionGuard } from '@flusys/nestjs-shared/guards';
|
|
1221
1051
|
|
|
1222
1052
|
// Interceptors
|
|
1223
1053
|
import {
|
|
@@ -1225,6 +1055,9 @@ import {
|
|
|
1225
1055
|
IdempotencyInterceptor,
|
|
1226
1056
|
SetCreatedByOnBody,
|
|
1227
1057
|
SetUpdateByOnBody,
|
|
1058
|
+
SetDeletedByOnBody,
|
|
1059
|
+
DeleteEmptyIdFromBodyInterceptor,
|
|
1060
|
+
QueryPerformanceInterceptor,
|
|
1228
1061
|
Slug,
|
|
1229
1062
|
} from '@flusys/nestjs-shared/interceptors';
|
|
1230
1063
|
|
|
@@ -1233,31 +1066,76 @@ import {
|
|
|
1233
1066
|
CacheModule,
|
|
1234
1067
|
DataSourceModule,
|
|
1235
1068
|
UtilsModule,
|
|
1069
|
+
UtilsService,
|
|
1070
|
+
MultiTenantDataSourceService,
|
|
1236
1071
|
} from '@flusys/nestjs-shared/modules';
|
|
1237
1072
|
|
|
1238
1073
|
// DTOs
|
|
1239
1074
|
import {
|
|
1240
1075
|
FilterAndPaginationDto,
|
|
1076
|
+
PaginationDto,
|
|
1241
1077
|
DeleteDto,
|
|
1242
|
-
|
|
1078
|
+
GetByIdBodyDto,
|
|
1079
|
+
SingleResponseDto,
|
|
1080
|
+
ListResponseDto,
|
|
1081
|
+
BulkResponseDto,
|
|
1082
|
+
MessageResponseDto,
|
|
1083
|
+
IdentityResponseDto,
|
|
1084
|
+
PaginationMetaDto,
|
|
1085
|
+
BulkMetaDto,
|
|
1086
|
+
RequestMetaDto,
|
|
1243
1087
|
} from '@flusys/nestjs-shared/dtos';
|
|
1244
1088
|
|
|
1245
1089
|
// Entities
|
|
1246
|
-
import {
|
|
1247
|
-
Identity,
|
|
1248
|
-
UserRoot,
|
|
1249
|
-
} from '@flusys/nestjs-shared/entities';
|
|
1090
|
+
import { Identity, UserRoot } from '@flusys/nestjs-shared/entities';
|
|
1250
1091
|
|
|
1251
1092
|
// Interfaces
|
|
1252
1093
|
import {
|
|
1253
1094
|
ILoggedUserInfo,
|
|
1254
|
-
|
|
1095
|
+
IService,
|
|
1096
|
+
IDataSourceProvider,
|
|
1097
|
+
IModuleConfigService,
|
|
1098
|
+
ILogger,
|
|
1099
|
+
PermissionCondition,
|
|
1100
|
+
PermissionOperator,
|
|
1255
1101
|
} from '@flusys/nestjs-shared/interfaces';
|
|
1256
1102
|
|
|
1257
1103
|
// Utilities
|
|
1258
1104
|
import {
|
|
1259
|
-
|
|
1105
|
+
getErrorMessage,
|
|
1106
|
+
logError,
|
|
1107
|
+
applyCompanyFilter,
|
|
1108
|
+
buildCompanyWhereCondition,
|
|
1109
|
+
validateCompanyOwnership,
|
|
1110
|
+
generateSlug,
|
|
1111
|
+
generateUniqueSlug,
|
|
1112
|
+
isBrowserRequest,
|
|
1113
|
+
buildCookieOptions,
|
|
1114
|
+
parseDurationToMs,
|
|
1115
|
+
escapeHtml,
|
|
1116
|
+
escapeHtmlVariables,
|
|
1260
1117
|
} from '@flusys/nestjs-shared/utils';
|
|
1118
|
+
|
|
1119
|
+
// Middleware
|
|
1120
|
+
import {
|
|
1121
|
+
LoggerMiddleware,
|
|
1122
|
+
getRequestId,
|
|
1123
|
+
getTenantId,
|
|
1124
|
+
getUserId,
|
|
1125
|
+
getCompanyId,
|
|
1126
|
+
setUserId,
|
|
1127
|
+
setCompanyId,
|
|
1128
|
+
} from '@flusys/nestjs-shared/middlewares';
|
|
1129
|
+
|
|
1130
|
+
// Exceptions
|
|
1131
|
+
import {
|
|
1132
|
+
InsufficientPermissionsException,
|
|
1133
|
+
NoPermissionsFoundException,
|
|
1134
|
+
PermissionSystemUnavailableException,
|
|
1135
|
+
} from '@flusys/nestjs-shared/exceptions';
|
|
1136
|
+
|
|
1137
|
+
// Constants
|
|
1138
|
+
import { PERMISSIONS } from '@flusys/nestjs-shared/constants';
|
|
1261
1139
|
```
|
|
1262
1140
|
|
|
1263
1141
|
---
|
|
@@ -1267,101 +1145,58 @@ import {
|
|
|
1267
1145
|
### 1. Use Generic Service Pattern
|
|
1268
1146
|
|
|
1269
1147
|
```typescript
|
|
1270
|
-
//
|
|
1148
|
+
// Extend ApiService for consistent CRUD
|
|
1271
1149
|
@Injectable()
|
|
1272
1150
|
export class ProductService extends ApiService<...> {
|
|
1273
1151
|
// Override hooks for customization
|
|
1274
1152
|
}
|
|
1275
1153
|
|
|
1276
|
-
//
|
|
1277
|
-
@Injectable()
|
|
1278
|
-
export class
|
|
1279
|
-
async getAll() { /* custom implementation */ }
|
|
1280
|
-
async getById() { /* custom implementation */ }
|
|
1281
|
-
// Inconsistent, error-prone
|
|
1282
|
-
}
|
|
1154
|
+
// For dynamic entities, use RequestScopedApiService
|
|
1155
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
1156
|
+
export class RoleService extends RequestScopedApiService<...> { }
|
|
1283
1157
|
```
|
|
1284
1158
|
|
|
1285
|
-
### 2. Use Decorators
|
|
1159
|
+
### 2. Always Use @Inject() Decorators
|
|
1160
|
+
|
|
1161
|
+
Required for esbuild bundled code:
|
|
1286
1162
|
|
|
1287
1163
|
```typescript
|
|
1288
|
-
//
|
|
1164
|
+
// CORRECT
|
|
1165
|
+
constructor(
|
|
1166
|
+
@Inject(MyService) private readonly myService: MyService,
|
|
1167
|
+
@Inject('CACHE_INSTANCE') private readonly cache: HybridCache,
|
|
1168
|
+
) {}
|
|
1169
|
+
|
|
1170
|
+
// WRONG - fails in bundled code
|
|
1171
|
+
constructor(private readonly myService: MyService) {}
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### 3. Use Decorators Consistently
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
// Use built-in decorators
|
|
1289
1178
|
@CurrentUser() user: ILoggedUserInfo
|
|
1290
|
-
@Public()
|
|
1291
1179
|
@RequirePermission('users.create')
|
|
1180
|
+
@Public() // Use sparingly!
|
|
1292
1181
|
|
|
1293
|
-
//
|
|
1294
|
-
@Req() req: Request
|
|
1295
|
-
const user = req.user; // Not type-safe
|
|
1182
|
+
// Don't access request directly
|
|
1183
|
+
@Req() req: Request // Not type-safe
|
|
1296
1184
|
```
|
|
1297
1185
|
|
|
1298
|
-
###
|
|
1186
|
+
### 4. Configure Security at Controller Level
|
|
1299
1187
|
|
|
1300
1188
|
```typescript
|
|
1301
|
-
//
|
|
1189
|
+
// GOOD - configure in createApiController
|
|
1302
1190
|
export class UserController extends createApiController(..., {
|
|
1303
|
-
security: { ... },
|
|
1191
|
+
security: { insert: 'permission', ... },
|
|
1304
1192
|
}) {}
|
|
1305
1193
|
|
|
1306
|
-
//
|
|
1194
|
+
// AVOID - adding guards to each endpoint
|
|
1307
1195
|
@UseGuards(JwtGuard)
|
|
1308
1196
|
@Post('create')
|
|
1309
1197
|
create() {}
|
|
1310
1198
|
```
|
|
1311
1199
|
|
|
1312
|
-
### 4. Use Cache for Repeated Queries
|
|
1313
|
-
|
|
1314
|
-
```typescript
|
|
1315
|
-
// ✅ Cache expensive operations
|
|
1316
|
-
async getPermissions(userId: string) {
|
|
1317
|
-
const cacheKey = `permissions:${userId}`;
|
|
1318
|
-
const cached = await this.cache.get(cacheKey);
|
|
1319
|
-
if (cached) return cached;
|
|
1320
|
-
|
|
1321
|
-
const permissions = await this.fetchPermissions(userId);
|
|
1322
|
-
await this.cache.set(cacheKey, permissions, 3600);
|
|
1323
|
-
return permissions;
|
|
1324
|
-
}
|
|
1325
|
-
```
|
|
1326
|
-
|
|
1327
|
-
---
|
|
1328
|
-
|
|
1329
|
-
## Dependencies
|
|
1330
|
-
|
|
1331
|
-
- `@nestjs/common`
|
|
1332
|
-
- `@nestjs/core`
|
|
1333
|
-
- `@nestjs/typeorm`
|
|
1334
|
-
- `@nestjs/swagger`
|
|
1335
|
-
- `typeorm`
|
|
1336
|
-
- `class-validator`
|
|
1337
|
-
- `class-transformer`
|
|
1338
|
-
- `@flusys/nestjs-core`
|
|
1339
|
-
|
|
1340
|
-
---
|
|
1341
|
-
|
|
1342
|
-
## Related Documentation
|
|
1343
|
-
|
|
1344
|
-
- [API Controller Security Guide](./API-CONTROLLER-SECURITY.md)
|
|
1345
|
-
- [Core Package Guide](./CORE-GUIDE.md)
|
|
1346
|
-
- [Auth Package Guide](./AUTH-GUIDE.md)
|
|
1347
|
-
|
|
1348
|
-
---
|
|
1349
|
-
|
|
1350
|
-
## Summary
|
|
1351
|
-
|
|
1352
|
-
The `@flusys/nestjs-shared` package provides:
|
|
1353
|
-
|
|
1354
|
-
- **Generic CRUD** - Consistent API patterns
|
|
1355
|
-
- **Permission System** - Flexible access control
|
|
1356
|
-
- **Caching** - High-performance data access
|
|
1357
|
-
- **Multi-Tenancy** - Database isolation
|
|
1358
|
-
- **Request Correlation** - AsyncLocalStorage-based tracking
|
|
1359
|
-
- **Middleware** - Logging and performance monitoring
|
|
1360
|
-
- **Interceptors** - Request/response processing
|
|
1361
|
-
- **Error Handling** - Centralized error management
|
|
1362
|
-
|
|
1363
|
-
This is the shared infrastructure layer used by all other Flusys packages.
|
|
1364
|
-
|
|
1365
1200
|
---
|
|
1366
1201
|
|
|
1367
|
-
**Last Updated:** 2026-02-
|
|
1202
|
+
**Last Updated:** 2026-02-21
|