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