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