@flusys/nestjs-shared 4.0.2 → 4.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 +448 -1561
- package/cjs/classes/api-controller.class.js +207 -11
- package/cjs/classes/api-service.class.js +40 -4
- package/cjs/dtos/get-by-ids.dto.js +65 -0
- package/cjs/dtos/index.js +1 -0
- package/cjs/entities/index.js +1 -0
- package/cjs/entities/raw-type.js +4 -0
- package/classes/api-controller.class.d.ts +9 -3
- package/classes/api-service.class.d.ts +6 -5
- package/dtos/get-by-ids.dto.d.ts +4 -0
- package/dtos/index.d.ts +1 -0
- package/entities/index.d.ts +1 -0
- package/entities/raw-type.d.ts +1 -0
- package/fesm/classes/api-controller.class.js +208 -12
- package/fesm/classes/api-service.class.js +40 -4
- package/fesm/dtos/get-by-ids.dto.js +55 -0
- package/fesm/dtos/index.js +1 -0
- package/fesm/entities/index.js +1 -0
- package/fesm/entities/raw-type.js +1 -0
- package/interfaces/api.interface.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,1858 +1,745 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flusys/nestjs-shared
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> **Version:** 4.0.2
|
|
5
|
-
> **Type:** Shared NestJS utilities, classes, decorators, guards, and modules
|
|
3
|
+
> Shared NestJS infrastructure — generic CRUD base classes, JWT guards, permission system, hybrid caching, structured logging, multi-tenancy, and response standardization for the entire FLUSYS ecosystem.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/nestjs-shared)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nestjs.com/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
8
12
|
|
|
9
13
|
## Table of Contents
|
|
10
14
|
|
|
11
15
|
- [Overview](#overview)
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Architecture Position](#architecture-position)
|
|
18
|
+
- [Compatibility](#compatibility)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Base Classes](#base-classes)
|
|
22
|
+
- [ApiService](#apiservice)
|
|
23
|
+
- [RequestScopedApiService](#requestscopedapiservice)
|
|
24
|
+
- [createApiController](#createapicontroller)
|
|
25
|
+
- [Base Entities](#base-entities)
|
|
26
|
+
- [Response DTOs](#response-dtos)
|
|
17
27
|
- [Guards](#guards)
|
|
18
|
-
- [
|
|
28
|
+
- [Decorators](#decorators)
|
|
19
29
|
- [Interceptors](#interceptors)
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [Error Handling](#error-handling)
|
|
26
|
-
- [Constants](#constants)
|
|
30
|
+
- [HybridCache](#hybridcache)
|
|
31
|
+
- [Middlewares](#middlewares)
|
|
32
|
+
- [Exceptions & Filters](#exceptions--filters)
|
|
33
|
+
- [Modules](#modules)
|
|
34
|
+
- [Multi-Tenant DataSource Service](#multi-tenant-datasource-service)
|
|
27
35
|
- [Logger System](#logger-system)
|
|
28
|
-
- [
|
|
36
|
+
- [Permission System](#permission-system)
|
|
37
|
+
- [Permission Constants](#permission-constants)
|
|
38
|
+
- [Cross-Module Adapter Interfaces](#cross-module-adapter-interfaces)
|
|
39
|
+
- [Troubleshooting](#troubleshooting)
|
|
40
|
+
- [License](#license)
|
|
29
41
|
|
|
30
42
|
---
|
|
31
43
|
|
|
32
44
|
## Overview
|
|
33
45
|
|
|
34
|
-
`@flusys/nestjs-shared`
|
|
46
|
+
`@flusys/nestjs-shared` is the shared infrastructure layer that all FLUSYS feature modules depend on. It provides the building blocks — generic CRUD, guards, decorators, DTOs, caching — so each feature module only needs to implement business logic.
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
- **Permission System** - Role and permission-based access control with complex logic and wildcard matching
|
|
38
|
-
- **Caching** - In-memory + Redis hybrid caching (HybridCache)
|
|
39
|
-
- **Request Correlation** - AsyncLocalStorage-based request tracking with correlation IDs
|
|
40
|
-
- **Middleware** - Logging, correlation, and performance monitoring
|
|
41
|
-
- **Interceptors** - Response metadata, idempotency, auto field setting
|
|
42
|
-
- **Multi-Tenancy** - Dynamic database connection management with connection pooling
|
|
43
|
-
- **Error Handling** - Centralized error handling with sensitive data redaction
|
|
44
|
-
- **Logging** - Winston-based logging with tenant-aware routing and daily rotation
|
|
48
|
+
---
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
## Features
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@flusys/nestjs-storage <- Uses common patterns
|
|
58
|
-
```
|
|
52
|
+
- **Generic CRUD** — `ApiService` + `createApiController` with 7 standardized POST-only RPC endpoints
|
|
53
|
+
- **Permission System** — AND/OR/nested logic trees with wildcard matching, action-level guards
|
|
54
|
+
- **Hybrid Cache** — In-memory (CacheableMemory) + Redis (`@keyv/redis`) with automatic L1/L2 fallback
|
|
55
|
+
- **Request Correlation** — `AsyncLocalStorage`-based request tracking with correlation IDs
|
|
56
|
+
- **Structured Logging** — Winston + daily-rotate-file with tenant-aware routing
|
|
57
|
+
- **Multi-Tenancy** — Dynamic database connection management with per-tenant DataSource pooling
|
|
58
|
+
- **Response Standardization** — Consistent `SingleResponseDto`, `ListResponseDto`, `BulkResponseDto`, `MessageResponseDto`
|
|
59
|
+
- **Security** — HTML sanitization (XSS prevention), slug generation, path traversal protection
|
|
60
|
+
- **Interceptors** — Response metadata, idempotency, audit fields, slug auto-generation
|
|
59
61
|
|
|
60
62
|
---
|
|
61
63
|
|
|
62
|
-
##
|
|
64
|
+
## Architecture Position
|
|
63
65
|
|
|
64
66
|
```
|
|
65
|
-
nestjs-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
│ │ ├── winston.logger.class.ts # Winston logger config with tenant routing
|
|
73
|
-
│ │ └── winston-logger-adapter.class.ts # ILogger adapters
|
|
74
|
-
│ │
|
|
75
|
-
│ ├── constants/ # Injection tokens & constants
|
|
76
|
-
│ │ ├── permissions.ts # Permission constants (PERMISSIONS)
|
|
77
|
-
│ │ └── index.ts # Metadata keys, injection tokens, headers
|
|
78
|
-
│ │
|
|
79
|
-
│ ├── decorators/ # Custom decorators
|
|
80
|
-
│ │ ├── api-response.decorator.ts # @ApiResponseDto
|
|
81
|
-
│ │ ├── current-user.decorator.ts # @CurrentUser
|
|
82
|
-
│ │ ├── public.decorator.ts # @Public
|
|
83
|
-
│ │ ├── require-permission.decorator.ts # @RequirePermission, @RequireAnyPermission, @RequirePermissionLogic
|
|
84
|
-
│ │ ├── sanitize-html.decorator.ts # @SanitizeHtml, @SanitizeAndTrim
|
|
85
|
-
│ │ └── index.ts
|
|
86
|
-
│ │
|
|
87
|
-
│ ├── dtos/ # Shared DTOs
|
|
88
|
-
│ │ ├── delete.dto.ts # DeleteDto with soft/restore/permanent
|
|
89
|
-
│ │ ├── filter-and-pagination.dto.ts # FilterAndPaginationDto, GetByIdBodyDto
|
|
90
|
-
│ │ ├── identity-response.dto.ts # IdentityResponseDto
|
|
91
|
-
│ │ ├── pagination.dto.ts # PaginationDto
|
|
92
|
-
│ │ ├── response-payload.dto.ts # Single/List/Bulk/Message response DTOs
|
|
93
|
-
│ │ └── index.ts
|
|
94
|
-
│ │
|
|
95
|
-
│ ├── entities/ # Base entities
|
|
96
|
-
│ │ ├── identity.ts # Base entity with UUID & audit fields
|
|
97
|
-
│ │ ├── user-root.ts # Base user entity
|
|
98
|
-
│ │ └── index.ts
|
|
99
|
-
│ │
|
|
100
|
-
│ ├── exceptions/ # Custom exceptions
|
|
101
|
-
│ │ └── permission.exception.ts # Permission-related exceptions
|
|
102
|
-
│ │
|
|
103
|
-
│ ├── guards/ # Authentication & authorization
|
|
104
|
-
│ │ ├── jwt-auth.guard.ts # JWT token validation with @Public support
|
|
105
|
-
│ │ └── permission.guard.ts # Permission checks with wildcard matching
|
|
106
|
-
│ │
|
|
107
|
-
│ ├── interceptors/ # Request/response interceptors
|
|
108
|
-
│ │ ├── delete-empty-id-from-body.interceptor.ts
|
|
109
|
-
│ │ ├── idempotency.interceptor.ts # Prevents duplicate POST requests
|
|
110
|
-
│ │ ├── query-performance.interceptor.ts
|
|
111
|
-
│ │ ├── response-meta.interceptor.ts # Adds _meta to responses
|
|
112
|
-
│ │ ├── set-user-field-on-body.interceptor.ts # Factory + SetCreatedByOnBody, etc.
|
|
113
|
-
│ │ └── slug.interceptor.ts # Auto-generates slug from name
|
|
114
|
-
│ │
|
|
115
|
-
│ ├── interfaces/ # TypeScript interfaces
|
|
116
|
-
│ │ ├── api.interface.ts # IService interface
|
|
117
|
-
│ │ ├── datasource.interface.ts # IDataSourceProvider
|
|
118
|
-
│ │ ├── identity.interface.ts # IIdentity interface
|
|
119
|
-
│ │ ├── logged-user-info.interface.ts # ILoggedUserInfo
|
|
120
|
-
│ │ ├── logger.interface.ts # ILogger interface
|
|
121
|
-
│ │ ├── module-config.interface.ts # IModuleConfigService
|
|
122
|
-
│ │ └── permission.interface.ts # ILogicNode, PermissionConfig, PermissionGuardConfig
|
|
123
|
-
│ │
|
|
124
|
-
│ ├── middlewares/ # Middleware
|
|
125
|
-
│ │ └── logger.middleware.ts # Request correlation with AsyncLocalStorage
|
|
126
|
-
│ │
|
|
127
|
-
│ ├── modules/ # NestJS modules
|
|
128
|
-
│ │ ├── cache/cache.module.ts # CacheModule.forRoot()
|
|
129
|
-
│ │ ├── datasource/
|
|
130
|
-
│ │ │ ├── datasource.module.ts # DataSourceModule.forRoot/forRootAsync/forFeature
|
|
131
|
-
│ │ │ └── multi-tenant-datasource.service.ts
|
|
132
|
-
│ │ └── utils/
|
|
133
|
-
│ │ ├── utils.module.ts # Global UtilsModule
|
|
134
|
-
│ │ └── utils.service.ts # Cache helpers, string utilities
|
|
135
|
-
│ │
|
|
136
|
-
│ └── utils/ # Utility functions
|
|
137
|
-
│ ├── html-sanitizer.util.ts # escapeHtml, escapeHtmlVariables
|
|
138
|
-
│ ├── query-helpers.util.ts # applyCompanyFilter, validateCompanyOwnership
|
|
139
|
-
│ ├── request.util.ts # isBrowserRequest, buildCookieOptions, parseDurationToMs
|
|
140
|
-
│ └── string.util.ts # generateSlug, generateUniqueSlug
|
|
67
|
+
@flusys/nestjs-core
|
|
68
|
+
↓
|
|
69
|
+
@flusys/nestjs-shared ← THIS PACKAGE
|
|
70
|
+
↓
|
|
71
|
+
┌───────┴───────────────────────┐
|
|
72
|
+
auth iam storage form-builder email event-manager notification localization
|
|
73
|
+
(all depend on nestjs-shared, none depend on each other)
|
|
141
74
|
```
|
|
142
75
|
|
|
143
76
|
---
|
|
144
77
|
|
|
145
|
-
##
|
|
78
|
+
## Compatibility
|
|
146
79
|
|
|
147
|
-
|
|
80
|
+
| Package | Version |
|
|
81
|
+
|---------|---------|
|
|
82
|
+
| `@flusys/nestjs-core` | `^4.0.0` |
|
|
83
|
+
| `@nestjs/core` | `^11.0.0` |
|
|
84
|
+
| `@nestjs/common` | `^11.0.0` |
|
|
85
|
+
| `@nestjs/passport` | `^10.0.0` |
|
|
86
|
+
| `typeorm` | `^0.3.0` |
|
|
87
|
+
| `winston` | `^3.0.0` |
|
|
88
|
+
| `keyv` | `^5.0.0` |
|
|
89
|
+
| Node.js | `>= 18.x` |
|
|
148
90
|
|
|
149
|
-
|
|
91
|
+
---
|
|
150
92
|
|
|
151
|
-
|
|
152
|
-
import { ApiService, HybridCache } from '@flusys/nestjs-shared';
|
|
153
|
-
import { UtilsService } from '@flusys/nestjs-shared';
|
|
154
|
-
import { Injectable, Inject } from '@nestjs/common';
|
|
155
|
-
import { InjectRepository } from '@nestjs/typeorm';
|
|
156
|
-
import { Repository } from 'typeorm';
|
|
157
|
-
import { CACHE_INSTANCE } from '@flusys/nestjs-shared';
|
|
93
|
+
## Installation
|
|
158
94
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
CreateUserDto, // Create DTO
|
|
162
|
-
UpdateUserDto, // Update DTO
|
|
163
|
-
IUser, // Interface
|
|
164
|
-
User, // Entity
|
|
165
|
-
Repository<User> // Repository type
|
|
166
|
-
> {
|
|
167
|
-
constructor(
|
|
168
|
-
@InjectRepository(User)
|
|
169
|
-
protected override repository: Repository<User>,
|
|
170
|
-
@Inject(CACHE_INSTANCE)
|
|
171
|
-
protected override cacheManager: HybridCache,
|
|
172
|
-
@Inject(UtilsService)
|
|
173
|
-
protected override utilsService: UtilsService,
|
|
174
|
-
) {
|
|
175
|
-
super(
|
|
176
|
-
'user', // Entity name (for query alias)
|
|
177
|
-
repository,
|
|
178
|
-
cacheManager,
|
|
179
|
-
utilsService,
|
|
180
|
-
'UserService', // Logger context name
|
|
181
|
-
true, // Enable caching
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
95
|
+
```bash
|
|
96
|
+
npm install @flusys/nestjs-shared @flusys/nestjs-core
|
|
185
97
|
```
|
|
186
98
|
|
|
187
|
-
### ApiService Methods
|
|
188
|
-
|
|
189
|
-
| Method | Signature | Description |
|
|
190
|
-
|--------|-----------|-------------|
|
|
191
|
-
| `insert` | `(dto, user) → Promise<T>` | Create single entity |
|
|
192
|
-
| `insertMany` | `(dtos[], user) → Promise<T[]>` | Create multiple entities |
|
|
193
|
-
| `findById` | `(id, user, select?) → Promise<T>` | Find entity by ID (throws if not found) |
|
|
194
|
-
| `findByIds` | `(ids[], user) → Promise<T[]>` | Find multiple by IDs |
|
|
195
|
-
| `getAll` | `(search, filterDto, user) → Promise<{data, total}>` | Get paginated list with filters |
|
|
196
|
-
| `update` | `(dto, user) → Promise<T>` | Update single entity |
|
|
197
|
-
| `updateMany` | `(dtos[], user) → Promise<T[]>` | Update multiple entities |
|
|
198
|
-
| `delete` | `(deleteDto, user) → Promise<null>` | Soft/permanent delete or restore |
|
|
199
|
-
| `clearCacheForAll` | `() → Promise<void>` | Clear all entity cache |
|
|
200
|
-
| `clearCacheForId` | `(entities[]) → Promise<void>` | Clear cache for specific entities |
|
|
201
|
-
|
|
202
|
-
### Customization Hooks
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
@Injectable()
|
|
206
|
-
export class UserService extends ApiService<...> {
|
|
207
|
-
// Repository initialization (for RequestScopedApiService)
|
|
208
|
-
protected async ensureRepositoryInitialized(): Promise<void> { }
|
|
209
|
-
|
|
210
|
-
// Convert DTO to entity (called for single item)
|
|
211
|
-
protected async convertSingleDtoToEntity(
|
|
212
|
-
dto: CreateDtoT | UpdateDtoT,
|
|
213
|
-
user: ILoggedUserInfo | null
|
|
214
|
-
): Promise<EntityT> { }
|
|
215
|
-
|
|
216
|
-
// Convert entity to response DTO
|
|
217
|
-
protected convertEntityToResponseDto(entity: EntityT, isRaw: boolean): InterfaceT { }
|
|
218
|
-
|
|
219
|
-
// Convert entity list to response list
|
|
220
|
-
protected convertEntityListToResponseListDto(entities: EntityT[], isRaw: boolean): InterfaceT[] { }
|
|
221
|
-
|
|
222
|
-
// Customize SELECT query (add joins, selections)
|
|
223
|
-
protected async getSelectQuery(
|
|
224
|
-
query: SelectQueryBuilder<EntityT>,
|
|
225
|
-
user: ILoggedUserInfo | null,
|
|
226
|
-
select?: string[]
|
|
227
|
-
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
228
|
-
|
|
229
|
-
// Add WHERE filters from filter object
|
|
230
|
-
protected async getFilterQuery(
|
|
231
|
-
query: SelectQueryBuilder<EntityT>,
|
|
232
|
-
filter: Record<string, any>,
|
|
233
|
-
user: ILoggedUserInfo | null
|
|
234
|
-
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
235
|
-
|
|
236
|
-
// Add global search conditions (default: searches 'name' field)
|
|
237
|
-
protected async getGlobalSearchQuery(
|
|
238
|
-
query: SelectQueryBuilder<EntityT>,
|
|
239
|
-
search: string,
|
|
240
|
-
user: ILoggedUserInfo | null
|
|
241
|
-
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
242
|
-
|
|
243
|
-
// Add sort conditions
|
|
244
|
-
protected async getSortQuery(
|
|
245
|
-
query: SelectQueryBuilder<EntityT>,
|
|
246
|
-
sort: Record<string, 'ASC' | 'DESC'>,
|
|
247
|
-
user: ILoggedUserInfo | null
|
|
248
|
-
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
249
|
-
|
|
250
|
-
// Add extra query conditions (e.g., company filtering)
|
|
251
|
-
protected async getExtraManipulateQuery(
|
|
252
|
-
query: SelectQueryBuilder<EntityT>,
|
|
253
|
-
dto: FilterAndPaginationDto,
|
|
254
|
-
user: ILoggedUserInfo | null
|
|
255
|
-
): Promise<{ query: SelectQueryBuilder<EntityT>; isRaw: boolean }> { }
|
|
256
|
-
|
|
257
|
-
// Lifecycle hooks
|
|
258
|
-
protected async beforeInsertOperation(dto, user, queryRunner): Promise<void> { }
|
|
259
|
-
protected async afterInsertOperation(entities[], user, queryRunner): Promise<void> { }
|
|
260
|
-
protected async beforeUpdateOperation(dto, user, queryRunner): Promise<void> { }
|
|
261
|
-
protected async afterUpdateOperation(entities[], user, queryRunner): Promise<void> { }
|
|
262
|
-
protected async beforeDeleteOperation(dto, user, queryRunner): Promise<void> { }
|
|
263
|
-
protected async afterDeleteOperation(entities[], user, queryRunner): Promise<void> { }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
99
|
---
|
|
267
100
|
|
|
268
|
-
##
|
|
101
|
+
## Quick Start
|
|
269
102
|
|
|
270
|
-
|
|
103
|
+
### 1. Register Global Modules
|
|
271
104
|
|
|
272
105
|
```typescript
|
|
273
|
-
import {
|
|
274
|
-
import {
|
|
275
|
-
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
276
|
-
import { DataSource, EntityTarget, Repository } from 'typeorm';
|
|
277
|
-
|
|
278
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
279
|
-
export class RoleService extends RequestScopedApiService<
|
|
280
|
-
CreateRoleDto,
|
|
281
|
-
UpdateRoleDto,
|
|
282
|
-
IRole,
|
|
283
|
-
RoleBase,
|
|
284
|
-
Repository<RoleBase>
|
|
285
|
-
> {
|
|
286
|
-
private actionRepository!: Repository<Action>;
|
|
106
|
+
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
107
|
+
import { CacheModule, UtilsModule, LoggerMiddleware } from '@flusys/nestjs-shared';
|
|
287
108
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// Required: Resolve which entity class to use based on runtime config
|
|
299
|
-
protected resolveEntity(): EntityTarget<RoleBase> {
|
|
300
|
-
return this.config.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Required: Return the DataSource provider for repository creation
|
|
304
|
-
protected getDataSourceProvider(): IDataSourceProvider {
|
|
305
|
-
return this.provider;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Override to initialize additional repositories alongside primary
|
|
309
|
-
protected override async ensureRepositoryInitialized(): Promise<void> {
|
|
310
|
-
await super.ensureRepositoryInitialized();
|
|
311
|
-
// Initialize additional repositories if needed
|
|
312
|
-
const repos = await this.initializeAdditionalRepositories([Action]);
|
|
313
|
-
this.actionRepository = repos[0];
|
|
109
|
+
@Module({
|
|
110
|
+
imports: [
|
|
111
|
+
CacheModule.forRoot(true), // true = global
|
|
112
|
+
UtilsModule,
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
export class AppModule implements NestModule {
|
|
116
|
+
configure(consumer: MiddlewareConsumer) {
|
|
117
|
+
consumer.apply(LoggerMiddleware).forRoutes('*');
|
|
314
118
|
}
|
|
315
119
|
}
|
|
316
120
|
```
|
|
317
121
|
|
|
318
|
-
###
|
|
319
|
-
|
|
320
|
-
| Method | Signature | Description |
|
|
321
|
-
|--------|-----------|-------------|
|
|
322
|
-
| `ensureRepositoryInitialized()` | `() → Promise<void>` | Auto-called before operations, initializes repository |
|
|
323
|
-
| `resolveEntity()` | `() → EntityTarget<EntityT>` | **Abstract** - Return entity class based on runtime config |
|
|
324
|
-
| `getDataSourceProvider()` | `() → IDataSourceProvider` | **Abstract** - Return DataSource provider |
|
|
325
|
-
| `initializeAdditionalRepositories()` | `(entities[]) → Promise<Repository[]>` | Initialize extra repositories |
|
|
326
|
-
| `getDataSourceForService()` | `() → Promise<DataSource>` | Get raw DataSource for transactions |
|
|
327
|
-
|
|
328
|
-
### IDataSourceProvider Interface
|
|
122
|
+
### 2. Register Global Exception Filter & Interceptors
|
|
329
123
|
|
|
330
124
|
```typescript
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
125
|
+
import { GlobalExceptionFilter, ResponseMetaInterceptor } from '@flusys/nestjs-shared';
|
|
126
|
+
|
|
127
|
+
async function bootstrap() {
|
|
128
|
+
const app = await NestFactory.create(AppModule);
|
|
129
|
+
app.useGlobalFilters(new GlobalExceptionFilter());
|
|
130
|
+
app.useGlobalInterceptors(new ResponseMetaInterceptor());
|
|
131
|
+
await app.listen(3000);
|
|
334
132
|
}
|
|
335
133
|
```
|
|
336
134
|
|
|
337
135
|
---
|
|
338
136
|
|
|
339
|
-
##
|
|
137
|
+
## Base Classes
|
|
340
138
|
|
|
341
|
-
|
|
139
|
+
### ApiService
|
|
342
140
|
|
|
343
|
-
|
|
141
|
+
Generic CRUD service for simple, static repositories (no dynamic entity switching):
|
|
344
142
|
|
|
345
143
|
```typescript
|
|
346
|
-
import {
|
|
347
|
-
import {
|
|
348
|
-
import {
|
|
349
|
-
|
|
350
|
-
@
|
|
351
|
-
@
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
144
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
145
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
146
|
+
import { Repository } from 'typeorm';
|
|
147
|
+
import { ApiService } from '@flusys/nestjs-shared/classes';
|
|
148
|
+
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
149
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
150
|
+
|
|
151
|
+
@Injectable()
|
|
152
|
+
export class ProductService extends ApiService<
|
|
153
|
+
CreateProductDto,
|
|
154
|
+
UpdateProductDto,
|
|
155
|
+
IProduct,
|
|
156
|
+
Product,
|
|
157
|
+
Repository<Product>
|
|
158
|
+
> {
|
|
159
|
+
constructor(
|
|
160
|
+
@InjectRepository(Product) repo: Repository<Product>,
|
|
161
|
+
@Inject('CACHE_INSTANCE') cacheManager: HybridCache,
|
|
162
|
+
@Inject(UtilsService) utilsService: UtilsService,
|
|
163
|
+
) {
|
|
164
|
+
super('product', repo, cacheManager, utilsService, ProductService.name);
|
|
361
165
|
}
|
|
362
166
|
}
|
|
363
167
|
```
|
|
364
168
|
|
|
365
|
-
|
|
169
|
+
**Built-in methods:**
|
|
366
170
|
|
|
367
|
-
|
|
|
368
|
-
|
|
369
|
-
|
|
|
370
|
-
|
|
|
371
|
-
|
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
171
|
+
| Method | Description |
|
|
172
|
+
|--------|-------------|
|
|
173
|
+
| `insert(dto, user)` | Create a single record |
|
|
174
|
+
| `insertMany(dtos, user)` | Bulk create records |
|
|
175
|
+
| `update(dto, user)` | Update a record by ID |
|
|
176
|
+
| `updateMany(dtos, user)` | Bulk update records |
|
|
177
|
+
| `delete(dto, user)` | Soft delete by ID |
|
|
178
|
+
| `getAll(filter, user)` | Paginated list with filtering |
|
|
179
|
+
| `getById(id, dto)` | Get single record by ID |
|
|
180
|
+
|
|
181
|
+
### RequestScopedApiService
|
|
376
182
|
|
|
377
|
-
|
|
183
|
+
For services that need **dynamic entity loading** per request (DataSource Provider pattern). Use this for all feature modules that have `WithCompany` entity variants:
|
|
378
184
|
|
|
379
185
|
```typescript
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
|
|
385
|
-
|
|
386
|
-
// Endpoint security config
|
|
387
|
-
interface EndpointSecurity {
|
|
388
|
-
level: SecurityLevel;
|
|
389
|
-
permissions?: string[]; // Required permissions
|
|
390
|
-
operator?: 'AND' | 'OR'; // How to combine permissions (default: AND)
|
|
391
|
-
logic?: IPermissionLogic; // Complex permission logic tree
|
|
392
|
-
}
|
|
186
|
+
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
187
|
+
import { RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
188
|
+
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
189
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
393
190
|
|
|
394
|
-
//
|
|
395
|
-
|
|
191
|
+
@Injectable({ scope: Scope.REQUEST }) // REQUEST scope required
|
|
192
|
+
export class ProductService extends RequestScopedApiService<
|
|
193
|
+
CreateProductDto,
|
|
194
|
+
UpdateProductDto,
|
|
195
|
+
IProduct,
|
|
196
|
+
ProductBase,
|
|
197
|
+
Repository<ProductBase>
|
|
198
|
+
> {
|
|
199
|
+
constructor(
|
|
200
|
+
@Inject('CACHE_INSTANCE') protected override cacheManager: HybridCache,
|
|
201
|
+
@Inject(UtilsService) protected override utilsService: UtilsService,
|
|
202
|
+
@Inject(ModuleConfigService) private readonly moduleConfig: ModuleConfigService,
|
|
203
|
+
@Inject(DataSourceProvider) private readonly dataSourceProvider: DataSourceProvider,
|
|
204
|
+
) {
|
|
205
|
+
super('product', null as any, cacheManager, utilsService, ProductService.name, true);
|
|
206
|
+
}
|
|
396
207
|
|
|
397
|
-
|
|
398
|
-
|
|
208
|
+
protected override async ensureRepositoryInitialized(): Promise<void> {
|
|
209
|
+
if (!this.repositoryInitialized) {
|
|
210
|
+
const entity = this.moduleConfig.isCompanyFeatureEnabled() ? ProductWithCompany : Product;
|
|
211
|
+
this.repository = await this.dataSourceProvider.getRepository(entity);
|
|
212
|
+
this.repositoryInitialized = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
399
215
|
}
|
|
400
216
|
```
|
|
401
217
|
|
|
402
|
-
###
|
|
218
|
+
### createApiController
|
|
403
219
|
|
|
404
|
-
|
|
405
|
-
// Global security - all endpoints require JWT
|
|
406
|
-
@Controller('users')
|
|
407
|
-
export class UserController extends createApiController(
|
|
408
|
-
CreateUserDto,
|
|
409
|
-
UpdateUserDto,
|
|
410
|
-
UserResponseDto,
|
|
411
|
-
{ security: 'jwt' },
|
|
412
|
-
) {}
|
|
413
|
-
|
|
414
|
-
// Per-endpoint security
|
|
415
|
-
@Controller('users')
|
|
416
|
-
export class UserController extends createApiController(
|
|
417
|
-
CreateUserDto,
|
|
418
|
-
UpdateUserDto,
|
|
419
|
-
UserResponseDto,
|
|
420
|
-
{
|
|
421
|
-
security: {
|
|
422
|
-
getAll: 'public', // No auth required
|
|
423
|
-
getById: 'jwt', // JWT required
|
|
424
|
-
insert: { level: 'permission', permissions: ['user.create'] },
|
|
425
|
-
update: { level: 'permission', permissions: ['user.update'] },
|
|
426
|
-
delete: { level: 'permission', permissions: ['user.delete'] },
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
) {}
|
|
220
|
+
Factory function that generates a fully-typed CRUD controller class:
|
|
430
221
|
|
|
431
|
-
|
|
432
|
-
{
|
|
433
|
-
|
|
434
|
-
getAll: { level: 'permission', permissions: ['user.read', 'admin'], operator: 'OR' },
|
|
435
|
-
},
|
|
436
|
-
}
|
|
222
|
+
```typescript
|
|
223
|
+
import { Controller, Inject } from '@nestjs/common';
|
|
224
|
+
import { createApiController } from '@flusys/nestjs-shared/classes';
|
|
437
225
|
|
|
438
|
-
|
|
439
|
-
|
|
226
|
+
@Controller('administration/products')
|
|
227
|
+
export class ProductController extends createApiController<
|
|
228
|
+
CreateProductDto,
|
|
229
|
+
UpdateProductDto,
|
|
230
|
+
ProductResponseDto,
|
|
231
|
+
IProduct,
|
|
232
|
+
ProductService
|
|
233
|
+
>(CreateProductDto, UpdateProductDto, ProductResponseDto, {
|
|
234
|
+
entityName: 'product',
|
|
440
235
|
security: {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
{ type: 'group', operator: 'OR', children: [
|
|
449
|
-
{ type: 'action', actionId: 'admin' },
|
|
450
|
-
{ type: 'action', actionId: 'manager' },
|
|
451
|
-
]},
|
|
452
|
-
],
|
|
453
|
-
},
|
|
454
|
-
},
|
|
236
|
+
insert: { level: 'permission', permissions: ['product.create'] },
|
|
237
|
+
insertMany: { level: 'permission', permissions: ['product.create'] },
|
|
238
|
+
getById: { level: 'permission', permissions: ['product.read'] },
|
|
239
|
+
getAll: { level: 'permission', permissions: ['product.read'] },
|
|
240
|
+
update: { level: 'permission', permissions: ['product.update'] },
|
|
241
|
+
updateMany: { level: 'permission', permissions: ['product.update'] },
|
|
242
|
+
delete: { level: 'permission', permissions: ['product.delete'] },
|
|
455
243
|
},
|
|
244
|
+
}) {
|
|
245
|
+
constructor(@Inject(ProductService) public override service: ProductService) {
|
|
246
|
+
super(service);
|
|
247
|
+
}
|
|
456
248
|
}
|
|
457
249
|
```
|
|
458
250
|
|
|
459
|
-
**
|
|
251
|
+
**Generated endpoints:**
|
|
460
252
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
253
|
+
| Route | Method | Permission |
|
|
254
|
+
|-------|--------|-----------|
|
|
255
|
+
| `POST /products/insert` | `insert` | configurable |
|
|
256
|
+
| `POST /products/insert-many` | `insertMany` | configurable |
|
|
257
|
+
| `POST /products/get-all` | `getAll` | configurable |
|
|
258
|
+
| `POST /products/get/:id` | `getById` | configurable |
|
|
259
|
+
| `POST /products/update` | `update` | configurable |
|
|
260
|
+
| `POST /products/update-many` | `updateMany` | configurable |
|
|
261
|
+
| `POST /products/delete` | `delete` | configurable |
|
|
464
262
|
|
|
465
|
-
|
|
263
|
+
**Security levels:**
|
|
466
264
|
|
|
467
|
-
|
|
265
|
+
| Level | Description |
|
|
266
|
+
|-------|-------------|
|
|
267
|
+
| `'public'` | No authentication required (`@Public()`) |
|
|
268
|
+
| `'authenticated'` | Requires valid JWT only |
|
|
269
|
+
| `'permission'` | Requires JWT + specific action permissions |
|
|
468
270
|
|
|
469
|
-
|
|
470
|
-
import { CurrentUser } from '@flusys/nestjs-shared';
|
|
471
|
-
import { ILoggedUserInfo } from '@flusys/nestjs-shared';
|
|
472
|
-
|
|
473
|
-
@Controller('profile')
|
|
474
|
-
export class ProfileController {
|
|
475
|
-
@Post('me')
|
|
476
|
-
getProfile(@CurrentUser() user: ILoggedUserInfo) {
|
|
477
|
-
return { userId: user.id, companyId: user.companyId };
|
|
478
|
-
}
|
|
271
|
+
---
|
|
479
272
|
|
|
480
|
-
|
|
481
|
-
@Post('id')
|
|
482
|
-
getUserId(@CurrentUser('id') userId: string) {
|
|
483
|
-
return { userId };
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
```
|
|
273
|
+
## Base Entities
|
|
487
274
|
|
|
488
|
-
###
|
|
275
|
+
### Identity (IdentityEntity)
|
|
489
276
|
|
|
490
|
-
|
|
277
|
+
Base entity for all FLUSYS entities. All entities must extend this:
|
|
491
278
|
|
|
492
279
|
```typescript
|
|
493
|
-
import {
|
|
280
|
+
import { Identity } from '@flusys/nestjs-shared/entities';
|
|
494
281
|
|
|
495
|
-
@
|
|
496
|
-
export class
|
|
497
|
-
@
|
|
498
|
-
|
|
499
|
-
login() { }
|
|
282
|
+
@Entity('products')
|
|
283
|
+
export class Product extends Identity {
|
|
284
|
+
@Column() name: string;
|
|
285
|
+
// id, createdAt, updatedAt, deletedAt inherited
|
|
500
286
|
}
|
|
501
287
|
```
|
|
502
288
|
|
|
503
|
-
|
|
289
|
+
**Fields:**
|
|
504
290
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
@RequirePermission('admin.dashboard')
|
|
514
|
-
@Post('dashboard')
|
|
515
|
-
getDashboard() { }
|
|
516
|
-
|
|
517
|
-
// Requires BOTH permissions
|
|
518
|
-
@RequirePermission('users.read', 'admin.access')
|
|
519
|
-
@Post('users')
|
|
520
|
-
getUsers() { }
|
|
521
|
-
}
|
|
522
|
-
```
|
|
291
|
+
| Column | Type | Description |
|
|
292
|
+
|--------|------|-------------|
|
|
293
|
+
| `id` | `uuid` | Primary key, auto-generated |
|
|
294
|
+
| `createdAt` | `timestamp` | Auto-set on insert |
|
|
295
|
+
| `updatedAt` | `timestamp` | Auto-updated on save |
|
|
296
|
+
| `deletedAt` | `timestamp \| null` | Soft delete timestamp |
|
|
297
|
+
| `createdBy` | `uuid \| null` | User ID that created the record |
|
|
298
|
+
| `updatedBy` | `uuid \| null` | User ID that last updated the record |
|
|
523
299
|
|
|
524
|
-
###
|
|
300
|
+
### UserRoot
|
|
525
301
|
|
|
526
|
-
|
|
302
|
+
Extended base entity for user entities. `@flusys/nestjs-auth`'s `User` extends this:
|
|
527
303
|
|
|
528
304
|
```typescript
|
|
529
|
-
import {
|
|
530
|
-
|
|
531
|
-
@Controller('reports')
|
|
532
|
-
export class ReportsController {
|
|
533
|
-
// Requires 'reports.view' OR 'reports.admin'
|
|
534
|
-
@RequireAnyPermission('reports.view', 'reports.admin')
|
|
535
|
-
@Post()
|
|
536
|
-
getReports() { }
|
|
537
|
-
}
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### @RequirePermissionLogic
|
|
305
|
+
import { UserRoot } from '@flusys/nestjs-shared/entities';
|
|
541
306
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
@Controller('sensitive')
|
|
548
|
-
export class SensitiveController {
|
|
549
|
-
// Complex: User needs 'users.read' AND ('admin' OR 'manager')
|
|
550
|
-
@RequirePermissionLogic({
|
|
551
|
-
type: 'group',
|
|
552
|
-
operator: 'AND',
|
|
553
|
-
children: [
|
|
554
|
-
{ type: 'action', actionId: 'users.read' },
|
|
555
|
-
{ type: 'group', operator: 'OR', children: [
|
|
556
|
-
{ type: 'action', actionId: 'admin' },
|
|
557
|
-
{ type: 'action', actionId: 'manager' },
|
|
558
|
-
]},
|
|
559
|
-
],
|
|
560
|
-
})
|
|
561
|
-
@Post('complex')
|
|
562
|
-
getComplexData() { }
|
|
307
|
+
@Entity('user')
|
|
308
|
+
export class User extends UserRoot {
|
|
309
|
+
// Inherits: id, email, name, password, phone, avatar,
|
|
310
|
+
// isEmailVerified, isPhoneVerified, isActive, isSystemUser,
|
|
311
|
+
// lastLoginAt, createdAt, updatedAt, deletedAt
|
|
563
312
|
}
|
|
564
313
|
```
|
|
565
314
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
Escape HTML entities for XSS prevention:
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
import { SanitizeHtml, SanitizeAndTrim } from '@flusys/nestjs-shared';
|
|
572
|
-
|
|
573
|
-
export class CreateCommentDto {
|
|
574
|
-
@SanitizeHtml()
|
|
575
|
-
@IsString()
|
|
576
|
-
content: string;
|
|
577
|
-
|
|
578
|
-
@SanitizeAndTrim() // Escapes HTML AND trims whitespace
|
|
579
|
-
@IsString()
|
|
580
|
-
title: string;
|
|
581
|
-
}
|
|
582
|
-
```
|
|
315
|
+
---
|
|
583
316
|
|
|
584
|
-
|
|
317
|
+
## Response DTOs
|
|
585
318
|
|
|
586
|
-
|
|
319
|
+
All responses follow a consistent structure:
|
|
587
320
|
|
|
588
321
|
```typescript
|
|
589
|
-
import {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// Basic usage - logs start, success (with duration), and errors
|
|
597
|
-
@LogAction({ action: 'user.create' })
|
|
598
|
-
async createUser(dto: CreateUserDto): Promise<User> {
|
|
599
|
-
return this.repository.save(dto);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// With all options
|
|
603
|
-
@LogAction({
|
|
604
|
-
action: 'user.update', // Action name (default: ClassName.methodName)
|
|
605
|
-
module: 'auth', // Override module routing
|
|
606
|
-
includeParams: true, // Log method parameters (sensitive data redacted)
|
|
607
|
-
includeResult: true, // Log result preview
|
|
608
|
-
logLevel: 'info' // 'debug' | 'info' | 'warn' (default: 'debug')
|
|
609
|
-
})
|
|
610
|
-
async updateUser(dto: UpdateUserDto): Promise<User> {
|
|
611
|
-
return this.repository.save(dto);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
322
|
+
import {
|
|
323
|
+
SingleResponseDto,
|
|
324
|
+
ListResponseDto,
|
|
325
|
+
BulkResponseDto,
|
|
326
|
+
MessageResponseDto,
|
|
327
|
+
} from '@flusys/nestjs-shared/dtos';
|
|
614
328
|
```
|
|
615
329
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
### @ApiResponseDto
|
|
624
|
-
|
|
625
|
-
Generates Swagger schema for response:
|
|
330
|
+
| DTO | Shape | Used for |
|
|
331
|
+
|-----|-------|---------|
|
|
332
|
+
| `SingleResponseDto<T>` | `{ success, message, messageKey?, data?, _meta? }` | insert, update, getById |
|
|
333
|
+
| `ListResponseDto<T>` | `{ success, message, messageKey?, data?, meta, _meta? }` | getAll |
|
|
334
|
+
| `BulkResponseDto<T>` | `{ success, message, messageKey?, data?, meta, _meta? }` | insertMany, updateMany |
|
|
335
|
+
| `MessageResponseDto` | `{ success, message, messageKey?, _meta? }` | delete, actions |
|
|
626
336
|
|
|
337
|
+
**PaginationMetaDto:**
|
|
627
338
|
```typescript
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
@Controller('users')
|
|
631
|
-
export class UserController {
|
|
632
|
-
@Post('get-all')
|
|
633
|
-
@ApiResponseDto(UserResponseDto, true, 'list') // Array with PaginationMetaDto
|
|
634
|
-
getAll() { }
|
|
635
|
-
|
|
636
|
-
@Post('insert')
|
|
637
|
-
@ApiResponseDto(UserResponseDto, false) // Single item
|
|
638
|
-
insert() { }
|
|
639
|
-
}
|
|
339
|
+
{ total: number; page: number; pageSize: number; count: number; hasMore?: boolean; totalPages?: number }
|
|
640
340
|
```
|
|
641
341
|
|
|
342
|
+
**messageKey:** Every response includes a `messageKey` string (e.g., `'auth.login_success'`) for frontend localization. The frontend can look up this key in its translation dictionary instead of relying on the English message text.
|
|
343
|
+
|
|
642
344
|
---
|
|
643
345
|
|
|
644
346
|
## Guards
|
|
645
347
|
|
|
646
348
|
### JwtAuthGuard
|
|
647
349
|
|
|
648
|
-
Validates JWT tokens
|
|
350
|
+
Validates Bearer JWT tokens on incoming requests:
|
|
649
351
|
|
|
650
352
|
```typescript
|
|
651
|
-
import { JwtAuthGuard } from '@flusys/nestjs-shared';
|
|
652
|
-
import { APP_GUARD } from '@nestjs/core';
|
|
353
|
+
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
653
354
|
|
|
654
|
-
|
|
655
|
-
@
|
|
656
|
-
|
|
657
|
-
})
|
|
658
|
-
export class AppModule {}
|
|
355
|
+
@UseGuards(JwtAuthGuard)
|
|
356
|
+
@Post('protected')
|
|
357
|
+
async protectedEndpoint(@CurrentUser() user: ILoggedUserInfo) { }
|
|
659
358
|
```
|
|
660
359
|
|
|
661
|
-
|
|
360
|
+
Applied globally by default in the FLUSYS app. Use `@Public()` to bypass.
|
|
662
361
|
|
|
663
362
|
### PermissionGuard
|
|
664
363
|
|
|
665
|
-
Checks
|
|
364
|
+
Checks that the authenticated user has the required action permission:
|
|
666
365
|
|
|
667
366
|
```typescript
|
|
668
|
-
import { PermissionGuard
|
|
367
|
+
import { PermissionGuard } from '@flusys/nestjs-shared/guards';
|
|
368
|
+
import { RequirePermission } from '@flusys/nestjs-shared/decorators';
|
|
669
369
|
|
|
670
|
-
@
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
provide: PERMISSION_GUARD_CONFIG,
|
|
675
|
-
useValue: {
|
|
676
|
-
enableCompanyFeature: true,
|
|
677
|
-
userPermissionKeyFormat: 'permissions:user:{userId}',
|
|
678
|
-
companyPermissionKeyFormat: 'permissions:company:{companyId}:branch:{branchId}:user:{userId}',
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
],
|
|
682
|
-
})
|
|
683
|
-
export class AppModule {}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
**PermissionGuardConfig Interface:**
|
|
687
|
-
|
|
688
|
-
```typescript
|
|
689
|
-
interface PermissionGuardConfig {
|
|
690
|
-
enableCompanyFeature?: boolean;
|
|
691
|
-
userPermissionKeyFormat?: string; // Default: 'permissions:user:{userId}'
|
|
692
|
-
companyPermissionKeyFormat?: string; // Default: 'permissions:company:{companyId}:branch:{branchId}:user:{userId}'
|
|
693
|
-
}
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
**Cache Key Formats:**
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
// Without company feature (enableCompanyFeature: false)
|
|
700
|
-
'permissions:user:{userId}'
|
|
701
|
-
|
|
702
|
-
// With company feature (enableCompanyFeature: true && user.companyId exists)
|
|
703
|
-
'permissions:company:{companyId}:branch:{branchId}:user:{userId}'
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
**Wildcard Permission Matching:**
|
|
707
|
-
|
|
708
|
-
The guard supports wildcard permissions for flexible access control:
|
|
709
|
-
|
|
710
|
-
```typescript
|
|
711
|
-
// User has permissions: ['user.*', 'admin']
|
|
712
|
-
|
|
713
|
-
// These checks will PASS:
|
|
714
|
-
@RequirePermission('user.create') // Matches 'user.*'
|
|
715
|
-
@RequirePermission('user.read') // Matches 'user.*'
|
|
716
|
-
@RequirePermission('admin') // Exact match
|
|
717
|
-
|
|
718
|
-
// Wildcard patterns:
|
|
719
|
-
'*' // Matches ALL permissions (super admin)
|
|
720
|
-
'user.*' // Matches 'user.create', 'user.read', 'user.update', etc.
|
|
721
|
-
'storage.*' // Matches 'storage.file.create', 'storage.folder.read', etc.
|
|
370
|
+
@UseGuards(JwtAuthGuard, PermissionGuard)
|
|
371
|
+
@RequirePermission('product.create')
|
|
372
|
+
@Post('insert')
|
|
373
|
+
async insert() { }
|
|
722
374
|
```
|
|
723
375
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
```typescript
|
|
727
|
-
import {
|
|
728
|
-
InsufficientPermissionsException, // 403 - Missing required permissions
|
|
729
|
-
NoPermissionsFoundException, // 403 - No permissions in cache for user
|
|
730
|
-
PermissionSystemUnavailableException, // 500 - Cache not available
|
|
731
|
-
} from '@flusys/nestjs-shared';
|
|
732
|
-
|
|
733
|
-
// Exception response formats:
|
|
734
|
-
// InsufficientPermissionsException (AND):
|
|
735
|
-
{
|
|
736
|
-
"success": false,
|
|
737
|
-
"message": "Missing required permissions: user.create, user.update",
|
|
738
|
-
"code": "INSUFFICIENT_PERMISSIONS",
|
|
739
|
-
"missingPermissions": ["user.create", "user.update"],
|
|
740
|
-
"operator": "AND"
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// InsufficientPermissionsException (OR):
|
|
744
|
-
{
|
|
745
|
-
"success": false,
|
|
746
|
-
"message": "Requires at least one of: admin, manager",
|
|
747
|
-
"code": "INSUFFICIENT_PERMISSIONS",
|
|
748
|
-
"missingPermissions": ["admin", "manager"],
|
|
749
|
-
"operator": "OR"
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// NoPermissionsFoundException:
|
|
753
|
-
{
|
|
754
|
-
"success": false,
|
|
755
|
-
"message": "No permissions found. Please contact administrator.",
|
|
756
|
-
"code": "NO_PERMISSIONS_FOUND"
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// PermissionSystemUnavailableException:
|
|
760
|
-
{
|
|
761
|
-
"success": false,
|
|
762
|
-
"message": "Permission system temporarily unavailable. Please try again later.",
|
|
763
|
-
"code": "PERMISSION_SYSTEM_UNAVAILABLE"
|
|
764
|
-
}
|
|
765
|
-
```
|
|
376
|
+
> **Note:** Always use `@Inject(Reflector)` in your guard constructors — esbuild bundling loses TypeScript metadata.
|
|
766
377
|
|
|
767
378
|
---
|
|
768
379
|
|
|
769
|
-
##
|
|
770
|
-
|
|
771
|
-
### LoggerMiddleware
|
|
772
|
-
|
|
773
|
-
Combined middleware for request correlation and HTTP logging using AsyncLocalStorage.
|
|
774
|
-
|
|
775
|
-
**Features:**
|
|
776
|
-
- Request ID generation/tracking (UUID or from `x-request-id` header)
|
|
777
|
-
- Tenant ID tracking (from `x-tenant-id` header)
|
|
778
|
-
- AsyncLocalStorage context for thread-safe access across async operations
|
|
779
|
-
- Automatic sensitive header redaction (authorization, cookie, x-api-key)
|
|
780
|
-
- Performance monitoring (warns on requests > 3s)
|
|
781
|
-
- Body truncation (max 1000 chars in logs)
|
|
782
|
-
- Excluded paths: `/health`, `/metrics`, `/favicon.ico`
|
|
380
|
+
## Decorators
|
|
783
381
|
|
|
784
|
-
|
|
785
|
-
|
|
382
|
+
| Decorator | Import | Description |
|
|
383
|
+
|-----------|--------|-------------|
|
|
384
|
+
| `@CurrentUser()` | `@flusys/nestjs-shared/decorators` | Inject full `ILoggedUserInfo` object |
|
|
385
|
+
| `@CurrentUser('id')` | `@flusys/nestjs-shared/decorators` | Inject a specific field from user info |
|
|
386
|
+
| `@Public()` | `@flusys/nestjs-shared/decorators` | Mark endpoint as public (skip JWT guard) |
|
|
387
|
+
| `@RequirePermission(action)` | `@flusys/nestjs-shared/decorators` | Require a specific action permission |
|
|
388
|
+
| `@RequireAnyPermission(...actions)` | `@flusys/nestjs-shared/decorators` | Require any one of listed permissions |
|
|
389
|
+
| `@SanitizeHtml()` | `@flusys/nestjs-shared/decorators` | Strip HTML tags from string fields |
|
|
390
|
+
| `@SanitizeAndTrim()` | `@flusys/nestjs-shared/decorators` | Strip HTML and trim whitespace |
|
|
391
|
+
| `@LogAction(description)` | `@flusys/nestjs-shared/decorators` | Log method calls for audit |
|
|
392
|
+
| `@ApiResponseDto(type)` | `@flusys/nestjs-shared/decorators` | Swagger response shorthand |
|
|
786
393
|
|
|
787
|
-
|
|
394
|
+
### ILoggedUserInfo
|
|
788
395
|
|
|
789
396
|
```typescript
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
configure(consumer: MiddlewareConsumer) {
|
|
795
|
-
consumer.apply(LoggerMiddleware).forRoutes('*');
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
**Request Context Interface:**
|
|
801
|
-
|
|
802
|
-
```typescript
|
|
803
|
-
interface IRequestContext {
|
|
804
|
-
requestId: string;
|
|
805
|
-
tenantId?: string;
|
|
806
|
-
userId?: string;
|
|
397
|
+
interface ILoggedUserInfo {
|
|
398
|
+
id: string;
|
|
399
|
+
email: string;
|
|
400
|
+
name: string;
|
|
807
401
|
companyId?: string;
|
|
808
|
-
|
|
402
|
+
branchId?: string;
|
|
403
|
+
permissions?: string[];
|
|
404
|
+
isSystemUser?: boolean;
|
|
809
405
|
}
|
|
810
406
|
```
|
|
811
407
|
|
|
812
|
-
**Accessing Request Context (read-only):**
|
|
813
|
-
|
|
814
|
-
```typescript
|
|
815
|
-
import {
|
|
816
|
-
getRequestId,
|
|
817
|
-
getTenantId,
|
|
818
|
-
getUserId,
|
|
819
|
-
getCompanyId,
|
|
820
|
-
requestContext, // AsyncLocalStorage instance for advanced usage
|
|
821
|
-
} from '@flusys/nestjs-shared';
|
|
822
|
-
|
|
823
|
-
@Injectable()
|
|
824
|
-
export class MyService {
|
|
825
|
-
async doSomething() {
|
|
826
|
-
const requestId = getRequestId(); // Current request's correlation ID
|
|
827
|
-
const tenantId = getTenantId(); // From x-tenant-id header
|
|
828
|
-
const userId = getUserId(); // Set by auth guard (if available)
|
|
829
|
-
const companyId = getCompanyId(); // Set by auth (if available)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
**Note:** The context values (`userId`, `companyId`) are populated by authentication guards after JWT validation. The middleware only initializes `requestId` and `tenantId` from headers.
|
|
835
|
-
|
|
836
408
|
---
|
|
837
409
|
|
|
838
410
|
## Interceptors
|
|
839
411
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
"data": [...],
|
|
849
|
-
"_meta": {
|
|
850
|
-
"requestId": "req_abc123def456",
|
|
851
|
-
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
852
|
-
"responseTime": 45
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
### IdempotencyInterceptor
|
|
858
|
-
|
|
859
|
-
Prevents duplicate POST requests using `X-Idempotency-Key` header.
|
|
860
|
-
|
|
861
|
-
**How it works:**
|
|
862
|
-
1. If key exists and completed → returns cached response (no reprocessing)
|
|
863
|
-
2. If key exists but processing → throws `ConflictException` (409)
|
|
864
|
-
3. If key is new → processes request and caches response for 24 hours
|
|
865
|
-
|
|
866
|
-
**Usage:**
|
|
867
|
-
|
|
868
|
-
```typescript
|
|
869
|
-
// Client includes header:
|
|
870
|
-
// X-Idempotency-Key: unique-order-123
|
|
871
|
-
|
|
872
|
-
// Controller (auto-applied by createApiController for insert endpoints)
|
|
873
|
-
@UseInterceptors(IdempotencyInterceptor)
|
|
874
|
-
@Post('create-order')
|
|
875
|
-
createOrder(@Body() dto: CreateOrderDto) { }
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
### SetCreatedByOnBody / SetUpdateByOnBody / SetDeletedByOnBody
|
|
879
|
-
|
|
880
|
-
Auto-set audit user IDs on request body from authenticated user.
|
|
881
|
-
|
|
882
|
-
```typescript
|
|
883
|
-
// Factory function for custom field names
|
|
884
|
-
import { createSetUserFieldInterceptor } from '@flusys/nestjs-shared';
|
|
885
|
-
|
|
886
|
-
// Built-in interceptors:
|
|
887
|
-
export const SetCreatedByOnBody = createSetUserFieldInterceptor('createdById');
|
|
888
|
-
export const SetUpdateByOnBody = createSetUserFieldInterceptor('updatedById');
|
|
889
|
-
export const SetDeletedByOnBody = createSetUserFieldInterceptor('deletedById');
|
|
890
|
-
|
|
891
|
-
// Usage:
|
|
892
|
-
@UseInterceptors(SetCreatedByOnBody)
|
|
893
|
-
@Post('insert')
|
|
894
|
-
insert(@Body() dto: CreateDto) { }
|
|
895
|
-
|
|
896
|
-
// Works with arrays too:
|
|
897
|
-
// Input: [{ name: 'A' }, { name: 'B' }]
|
|
898
|
-
// Output: [{ name: 'A', createdById: 'user-123' }, { name: 'B', createdById: 'user-123' }]
|
|
899
|
-
```
|
|
900
|
-
|
|
901
|
-
### DeleteEmptyIdFromBodyInterceptor
|
|
902
|
-
|
|
903
|
-
Removes empty/null `id` fields from request body (single objects and arrays).
|
|
904
|
-
|
|
905
|
-
```typescript
|
|
906
|
-
// Input: { id: '', name: 'Test' }
|
|
907
|
-
// Output: { name: 'Test' }
|
|
908
|
-
|
|
909
|
-
// Input: [{ id: null, name: 'A' }, { id: '123', name: 'B' }]
|
|
910
|
-
// Output: [{ name: 'A' }, { id: '123', name: 'B' }]
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
### Slug Interceptor
|
|
914
|
-
|
|
915
|
-
Auto-generates `slug` from `name` field if not provided.
|
|
916
|
-
|
|
917
|
-
```typescript
|
|
918
|
-
import { Slug } from '@flusys/nestjs-shared';
|
|
919
|
-
|
|
920
|
-
@UseInterceptors(Slug)
|
|
921
|
-
@Post('insert')
|
|
922
|
-
insert(@Body() dto: CreateDto) { }
|
|
923
|
-
|
|
924
|
-
// Input: { name: 'My Product Name' }
|
|
925
|
-
// Output: { name: 'My Product Name', slug: 'my-product-name' }
|
|
926
|
-
|
|
927
|
-
// If slug already provided, it's preserved:
|
|
928
|
-
// Input: { name: 'My Product', slug: 'custom-slug' }
|
|
929
|
-
// Output: { name: 'My Product', slug: 'custom-slug' }
|
|
930
|
-
```
|
|
412
|
+
| Interceptor | Token | Description |
|
|
413
|
+
|-------------|-------|-------------|
|
|
414
|
+
| `ResponseMetaInterceptor` | class | Adds `_meta` (requestId, timestamp, duration) to all responses |
|
|
415
|
+
| `IdempotencyInterceptor` | class | Prevents duplicate POST requests using `x-idempotency-key` header |
|
|
416
|
+
| `SetCreatedByInterceptor` | class | Auto-sets `createdBy` from JWT user on insert |
|
|
417
|
+
| `SetUpdatedByInterceptor` | class | Auto-sets `updatedBy` from JWT user on update |
|
|
418
|
+
| `DeleteEmptyIdInterceptor` | class | Strips empty `id` fields from request bodies |
|
|
419
|
+
| `SlugInterceptor` | class | Auto-generates URL slugs from `name` fields |
|
|
931
420
|
|
|
932
421
|
---
|
|
933
422
|
|
|
934
|
-
##
|
|
423
|
+
## HybridCache
|
|
935
424
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
Two-tier caching with in-memory (L1) and Redis (L2):
|
|
425
|
+
Two-tier cache: L1 in-memory (CacheableMemory) → L2 Redis. Automatically falls back to in-memory if Redis is unavailable.
|
|
939
426
|
|
|
940
427
|
```typescript
|
|
941
|
-
import { HybridCache } from '@flusys/nestjs-shared';
|
|
428
|
+
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
942
429
|
|
|
943
430
|
@Injectable()
|
|
944
431
|
export class MyService {
|
|
945
432
|
constructor(@Inject('CACHE_INSTANCE') private cache: HybridCache) {}
|
|
946
433
|
|
|
947
|
-
async getData(key: string) {
|
|
948
|
-
// Check cache first
|
|
434
|
+
async getData(key: string): Promise<any> {
|
|
949
435
|
const cached = await this.cache.get(key);
|
|
950
436
|
if (cached) return cached;
|
|
951
437
|
|
|
952
438
|
const data = await this.fetchFromDb();
|
|
953
|
-
await this.cache.set(key, data, 3600);
|
|
439
|
+
await this.cache.set(key, data, 3600); // TTL in seconds
|
|
954
440
|
return data;
|
|
955
441
|
}
|
|
956
442
|
|
|
957
|
-
async invalidate(key: string) {
|
|
958
|
-
await this.cache.
|
|
443
|
+
async invalidate(key: string): Promise<void> {
|
|
444
|
+
await this.cache.delete(key);
|
|
959
445
|
}
|
|
960
446
|
|
|
961
|
-
async
|
|
962
|
-
await this.cache.
|
|
963
|
-
await this.cache.resetL2(); // Clear L2 (Redis)
|
|
447
|
+
async invalidateByPrefix(prefix: string): Promise<void> {
|
|
448
|
+
await this.cache.deleteByPrefix(prefix);
|
|
964
449
|
}
|
|
965
450
|
}
|
|
966
451
|
```
|
|
967
452
|
|
|
968
|
-
### CacheModule Setup
|
|
969
|
-
|
|
970
|
-
```typescript
|
|
971
|
-
import { CacheModule } from '@flusys/nestjs-shared';
|
|
972
|
-
|
|
973
|
-
@Module({
|
|
974
|
-
imports: [
|
|
975
|
-
CacheModule.forRoot(
|
|
976
|
-
true, // isGlobal
|
|
977
|
-
60_000, // memoryTtl (ms)
|
|
978
|
-
5000 // memorySize (LRU max items)
|
|
979
|
-
),
|
|
980
|
-
],
|
|
981
|
-
})
|
|
982
|
-
export class AppModule {}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
**Configuration via `USE_CACHE_LABEL` env:**
|
|
986
|
-
- `'memory'` - L1 only
|
|
987
|
-
- `'redis'` - L2 only
|
|
988
|
-
- `'hybrid'` - Both (default)
|
|
989
|
-
|
|
990
453
|
---
|
|
991
454
|
|
|
992
|
-
##
|
|
455
|
+
## Middlewares
|
|
993
456
|
|
|
994
|
-
|
|
457
|
+
### LoggerMiddleware
|
|
995
458
|
|
|
996
|
-
|
|
459
|
+
HTTP request/response logger with correlation IDs. Attach to all routes:
|
|
997
460
|
|
|
998
461
|
```typescript
|
|
999
|
-
import {
|
|
462
|
+
import { LoggerMiddleware } from '@flusys/nestjs-shared/middlewares';
|
|
1000
463
|
|
|
1001
|
-
@Module({
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
host: 'localhost',
|
|
1008
|
-
port: 3306,
|
|
1009
|
-
username: 'root',
|
|
1010
|
-
password: 'password',
|
|
1011
|
-
},
|
|
1012
|
-
tenants: [
|
|
1013
|
-
{ id: 'tenant1', database: 'tenant1_db' },
|
|
1014
|
-
{ id: 'tenant2', database: 'tenant2_db', host: 'other-server.com' },
|
|
1015
|
-
],
|
|
1016
|
-
}),
|
|
1017
|
-
],
|
|
1018
|
-
})
|
|
1019
|
-
export class AppModule {}
|
|
464
|
+
@Module({})
|
|
465
|
+
export class AppModule implements NestModule {
|
|
466
|
+
configure(consumer: MiddlewareConsumer) {
|
|
467
|
+
consumer.apply(LoggerMiddleware).forRoutes('*');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
1020
470
|
```
|
|
1021
471
|
|
|
1022
|
-
|
|
472
|
+
Logged fields: method, URL, status, duration, requestId, userId (from JWT), IP, user-agent.
|
|
1023
473
|
|
|
1024
|
-
|
|
1025
|
-
|--------|-------------|
|
|
1026
|
-
| `getDataSource()` | Get DataSource for current tenant (from header) |
|
|
1027
|
-
| `getDataSourceForTenant(id)` | Get DataSource for specific tenant |
|
|
1028
|
-
| `getRepository(entity)` | Get repository for current tenant |
|
|
1029
|
-
| `withTenant(id, callback)` | Execute callback with specific tenant |
|
|
1030
|
-
| `forAllTenants(callback)` | Execute callback for all tenants |
|
|
1031
|
-
| `registerTenant(config)` | Register new tenant at runtime |
|
|
1032
|
-
| `removeTenant(id)` | Remove tenant and close connection |
|
|
1033
|
-
| `getCurrentTenantId()` | Get tenant ID from request header |
|
|
474
|
+
The middleware also sets an `AsyncLocalStorage` context so `requestId` and `userId` are available in any service without passing them through method parameters.
|
|
1034
475
|
|
|
1035
476
|
---
|
|
1036
477
|
|
|
1037
|
-
##
|
|
478
|
+
## Exceptions & Filters
|
|
1038
479
|
|
|
1039
|
-
###
|
|
480
|
+
### Built-in Exception Classes
|
|
1040
481
|
|
|
1041
482
|
```typescript
|
|
1042
|
-
import {
|
|
483
|
+
import {
|
|
484
|
+
NotFoundException,
|
|
485
|
+
ConflictException,
|
|
486
|
+
UnauthorizedException,
|
|
487
|
+
ForbiddenException,
|
|
488
|
+
BadRequestException,
|
|
489
|
+
ValidationException,
|
|
490
|
+
} from '@flusys/nestjs-shared/exceptions';
|
|
1043
491
|
|
|
1044
|
-
//
|
|
1045
|
-
{
|
|
1046
|
-
"filter": { "status": "active" },
|
|
1047
|
-
"pagination": { "currentPage": 0, "pageSize": 10 },
|
|
1048
|
-
"sort": { "createdAt": "DESC" },
|
|
1049
|
-
"select": ["id", "name", "email"],
|
|
1050
|
-
"withDeleted": false
|
|
1051
|
-
}
|
|
492
|
+
// All exceptions accept { message, messageKey } for localization
|
|
493
|
+
throw new NotFoundException({ message: 'User not found', messageKey: 'user.not_found' });
|
|
1052
494
|
```
|
|
1053
495
|
|
|
1054
|
-
###
|
|
1055
|
-
|
|
1056
|
-
```typescript
|
|
1057
|
-
import { DeleteDto } from '@flusys/nestjs-shared';
|
|
1058
|
-
|
|
1059
|
-
// Soft delete
|
|
1060
|
-
{ "id": "uuid", "type": "delete" }
|
|
1061
|
-
|
|
1062
|
-
// Restore
|
|
1063
|
-
{ "id": "uuid", "type": "restore" }
|
|
1064
|
-
|
|
1065
|
-
// Permanent delete
|
|
1066
|
-
{ "id": "uuid", "type": "permanent" }
|
|
1067
|
-
|
|
1068
|
-
// Multiple IDs
|
|
1069
|
-
{ "id": ["uuid1", "uuid2"], "type": "delete" }
|
|
1070
|
-
```
|
|
496
|
+
### GlobalExceptionFilter
|
|
1071
497
|
|
|
1072
|
-
|
|
498
|
+
Catches all unhandled exceptions and returns a consistent error response:
|
|
1073
499
|
|
|
1074
500
|
```typescript
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
message: string;
|
|
1079
|
-
data?: T;
|
|
1080
|
-
_meta?: RequestMetaDto;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Paginated list
|
|
1084
|
-
class ListResponseDto<T> {
|
|
1085
|
-
success: boolean;
|
|
1086
|
-
message: string;
|
|
1087
|
-
data?: T[];
|
|
1088
|
-
meta: PaginationMetaDto; // { total, page, pageSize, count, hasMore?, totalPages? }
|
|
1089
|
-
_meta?: RequestMetaDto;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// Bulk operations
|
|
1093
|
-
class BulkResponseDto<T> {
|
|
1094
|
-
success: boolean;
|
|
1095
|
-
message: string;
|
|
1096
|
-
data?: T[];
|
|
1097
|
-
meta: BulkMetaDto; // { count, failed?, total? }
|
|
1098
|
-
_meta?: RequestMetaDto;
|
|
1099
|
-
}
|
|
501
|
+
// Register globally
|
|
502
|
+
app.useGlobalFilters(new GlobalExceptionFilter());
|
|
503
|
+
```
|
|
1100
504
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
505
|
+
Error response shape:
|
|
506
|
+
```json
|
|
507
|
+
{
|
|
508
|
+
"success": false,
|
|
509
|
+
"message": "User not found",
|
|
510
|
+
"messageKey": "user.not_found",
|
|
511
|
+
"code": 404,
|
|
512
|
+
"_meta": { "requestId": "...", "timestamp": "..." }
|
|
1106
513
|
}
|
|
1107
514
|
```
|
|
1108
515
|
|
|
1109
516
|
---
|
|
1110
517
|
|
|
1111
|
-
##
|
|
1112
|
-
|
|
1113
|
-
### Identity Entity
|
|
518
|
+
## Modules
|
|
1114
519
|
|
|
1115
|
-
|
|
520
|
+
### CacheModule
|
|
1116
521
|
|
|
1117
522
|
```typescript
|
|
1118
|
-
import {
|
|
523
|
+
import { CacheModule } from '@flusys/nestjs-shared/modules';
|
|
1119
524
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
// Inherited: id, createdAt, updatedAt, deletedAt
|
|
1123
|
-
// Inherited: createdById, updatedById, deletedById
|
|
525
|
+
// Global (register once in AppModule)
|
|
526
|
+
CacheModule.forRoot(true)
|
|
1124
527
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
}
|
|
528
|
+
// Local (non-global)
|
|
529
|
+
CacheModule.forRoot(false)
|
|
1128
530
|
```
|
|
1129
531
|
|
|
1130
|
-
|
|
532
|
+
Provides `HybridCache` as `'CACHE_INSTANCE'` injection token. Automatically connects to Redis if `REDIS_URL` is set.
|
|
1131
533
|
|
|
1132
|
-
|
|
534
|
+
### UtilsModule
|
|
1133
535
|
|
|
1134
536
|
```typescript
|
|
1135
|
-
import {
|
|
537
|
+
import { UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
1136
538
|
|
|
1137
|
-
@
|
|
1138
|
-
export class
|
|
1139
|
-
// Inherited: id, name, email, phone, profilePictureId
|
|
1140
|
-
// Inherited: isActive, emailVerified, phoneVerified
|
|
1141
|
-
// Inherited: lastLoginAt, additionalFields
|
|
1142
|
-
// Inherited: createdAt, updatedAt, deletedAt
|
|
1143
|
-
}
|
|
539
|
+
@Module({ imports: [UtilsModule] })
|
|
540
|
+
export class AppModule {}
|
|
1144
541
|
```
|
|
1145
542
|
|
|
1146
|
-
|
|
543
|
+
Provides `UtilsService` with slug generation, HTML sanitization, and query helper utilities.
|
|
1147
544
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
### Query Helpers
|
|
545
|
+
### DataSourceModule
|
|
1151
546
|
|
|
1152
547
|
```typescript
|
|
1153
|
-
import {
|
|
1154
|
-
applyCompanyFilter,
|
|
1155
|
-
buildCompanyWhereCondition,
|
|
1156
|
-
hasCompanyId,
|
|
1157
|
-
validateCompanyOwnership,
|
|
1158
|
-
} from '@flusys/nestjs-shared';
|
|
1159
|
-
|
|
1160
|
-
// Add company filter to TypeORM query
|
|
1161
|
-
applyCompanyFilter(query, {
|
|
1162
|
-
isCompanyFeatureEnabled: true,
|
|
1163
|
-
entityAlias: 'entity',
|
|
1164
|
-
}, user);
|
|
1165
|
-
|
|
1166
|
-
// Build where condition for company
|
|
1167
|
-
const where = buildCompanyWhereCondition(baseWhere, user, isCompanyFeatureEnabled);
|
|
1168
|
-
|
|
1169
|
-
// Validate entity belongs to user's company
|
|
1170
|
-
validateCompanyOwnership(entity, user, 'Entity');
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
### String Utilities
|
|
548
|
+
import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
1174
549
|
|
|
1175
|
-
|
|
1176
|
-
import { generateSlug, generateUniqueSlug } from '@flusys/nestjs-shared';
|
|
1177
|
-
|
|
1178
|
-
// Generate URL-friendly slug
|
|
1179
|
-
const slug = generateSlug('My Product Name', 100);
|
|
1180
|
-
// Returns: 'my-product-name'
|
|
1181
|
-
|
|
1182
|
-
// Generate unique slug with collision detection
|
|
1183
|
-
const uniqueSlug = await generateUniqueSlug(
|
|
1184
|
-
'My Product',
|
|
1185
|
-
async (pattern) => existingSlugs.filter(s => s.startsWith(pattern)),
|
|
1186
|
-
100
|
|
1187
|
-
);
|
|
550
|
+
DataSourceModule.forRoot(dataSourceOptions)
|
|
1188
551
|
```
|
|
1189
552
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
```typescript
|
|
1193
|
-
import {
|
|
1194
|
-
isBrowserRequest,
|
|
1195
|
-
buildCookieOptions,
|
|
1196
|
-
parseDurationToMs,
|
|
1197
|
-
} from '@flusys/nestjs-shared';
|
|
1198
|
-
|
|
1199
|
-
// Detect browser vs API client
|
|
1200
|
-
const isBrowser = isBrowserRequest(req);
|
|
1201
|
-
|
|
1202
|
-
// Build secure cookie options
|
|
1203
|
-
const cookieOpts = buildCookieOptions(req);
|
|
1204
|
-
|
|
1205
|
-
// Parse duration string
|
|
1206
|
-
const ms = parseDurationToMs('7d'); // 604800000
|
|
1207
|
-
```
|
|
1208
|
-
|
|
1209
|
-
### HTML Sanitizer
|
|
1210
|
-
|
|
1211
|
-
```typescript
|
|
1212
|
-
import { escapeHtml, escapeHtmlVariables } from '@flusys/nestjs-shared';
|
|
1213
|
-
|
|
1214
|
-
// Escape single string
|
|
1215
|
-
const safe = escapeHtml('<script>alert("xss")</script>');
|
|
1216
|
-
// Returns: '<script>alert("xss")</script>'
|
|
1217
|
-
|
|
1218
|
-
// Escape all values in an object
|
|
1219
|
-
const safeVars = escapeHtmlVariables({
|
|
1220
|
-
userName: '<script>evil</script>',
|
|
1221
|
-
message: 'Hello, World!',
|
|
1222
|
-
});
|
|
1223
|
-
```
|
|
553
|
+
Wraps `MultiTenantDataSourceService` for dynamic tenant-based DataSource resolution.
|
|
1224
554
|
|
|
1225
555
|
---
|
|
1226
556
|
|
|
1227
|
-
##
|
|
557
|
+
## Multi-Tenant DataSource Service
|
|
1228
558
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
Structured exceptions with i18n support and validation errors:
|
|
559
|
+
`MultiTenantDataSourceService` manages separate TypeORM DataSource connections per tenant. Each feature module extends it to create an isolated static connection cache:
|
|
1232
560
|
|
|
1233
561
|
```typescript
|
|
1234
|
-
import {
|
|
1235
|
-
BaseAppException,
|
|
1236
|
-
NotFoundException,
|
|
1237
|
-
ValidationException,
|
|
1238
|
-
UnauthorizedException,
|
|
1239
|
-
ForbiddenException,
|
|
1240
|
-
ConflictException,
|
|
1241
|
-
InternalServerException,
|
|
1242
|
-
ServiceUnavailableException,
|
|
1243
|
-
IValidationError,
|
|
1244
|
-
} from '@flusys/nestjs-shared';
|
|
1245
|
-
|
|
1246
|
-
// Custom exception with all options
|
|
1247
|
-
throw new BaseAppException({
|
|
1248
|
-
message: 'User registration failed',
|
|
1249
|
-
code: 'REGISTRATION_FAILED',
|
|
1250
|
-
messageKey: 'error.registration.failed', // For i18n
|
|
1251
|
-
status: HttpStatus.BAD_REQUEST,
|
|
1252
|
-
errors: [
|
|
1253
|
-
{ field: 'email', message: 'Email already exists', code: 'DUPLICATE_EMAIL' }
|
|
1254
|
-
],
|
|
1255
|
-
metadata: { attemptedEmail: email },
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
// Pre-built exceptions
|
|
1259
|
-
throw new NotFoundException('User', userId);
|
|
1260
|
-
// → { message: "User with id 'abc' not found", code: "NOT_FOUND", messageKey: "error.notFound" }
|
|
562
|
+
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
1261
563
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
throw new ForbiddenException('Admin access required');
|
|
1269
|
-
throw new ConflictException('User', 'email');
|
|
1270
|
-
throw new InternalServerException('Database connection failed');
|
|
1271
|
-
throw new ServiceUnavailableException('Email Service');
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
### GlobalExceptionFilter
|
|
1275
|
-
|
|
1276
|
-
Unified exception handling with structured responses and logging:
|
|
1277
|
-
|
|
1278
|
-
```typescript
|
|
1279
|
-
import { GlobalExceptionFilter } from '@flusys/nestjs-shared';
|
|
1280
|
-
|
|
1281
|
-
// Register in main.ts
|
|
1282
|
-
app.useGlobalFilters(new GlobalExceptionFilter());
|
|
1283
|
-
|
|
1284
|
-
// Or in app.module.ts
|
|
1285
|
-
@Module({
|
|
1286
|
-
providers: [
|
|
1287
|
-
{ provide: APP_FILTER, useClass: GlobalExceptionFilter },
|
|
1288
|
-
],
|
|
1289
|
-
})
|
|
1290
|
-
export class AppModule {}
|
|
1291
|
-
```
|
|
564
|
+
// Feature module DataSource Provider (extends with isolated cache)
|
|
565
|
+
@Injectable()
|
|
566
|
+
export class ProductDataSourceProvider extends MultiTenantDataSourceService {
|
|
567
|
+
// Static cache isolated to this module
|
|
568
|
+
private static moduleDataSource: DataSource | null = null;
|
|
569
|
+
private static moduleTenantSources: Map<string, DataSource> = new Map();
|
|
1292
570
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
{
|
|
1296
|
-
"success": false,
|
|
1297
|
-
"message": "User with id 'abc' not found",
|
|
1298
|
-
"code": "NOT_FOUND",
|
|
1299
|
-
"messageKey": "error.notFound",
|
|
1300
|
-
"errors": [...],
|
|
1301
|
-
"_meta": {
|
|
1302
|
-
"requestId": "uuid",
|
|
1303
|
-
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
1304
|
-
"responseTime": 45
|
|
571
|
+
protected override getSingleDataSource(): DataSource | null {
|
|
572
|
+
return ProductDataSourceProvider.moduleDataSource;
|
|
1305
573
|
}
|
|
574
|
+
|
|
575
|
+
// ... override other cache accessors
|
|
1306
576
|
}
|
|
1307
577
|
```
|
|
1308
578
|
|
|
1309
|
-
**Features:**
|
|
1310
|
-
- Consistent error response format
|
|
1311
|
-
- Request correlation (requestId, userId, tenantId)
|
|
1312
|
-
- Log level by status (500+ → error, 400+ → warn)
|
|
1313
|
-
- Handles BaseAppException, HttpException, and generic errors
|
|
1314
|
-
- class-validator error transformation
|
|
1315
|
-
|
|
1316
579
|
---
|
|
1317
580
|
|
|
1318
|
-
##
|
|
1319
|
-
|
|
1320
|
-
### Metadata Keys
|
|
1321
|
-
|
|
1322
|
-
```typescript
|
|
1323
|
-
import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from '@flusys/nestjs-shared';
|
|
1324
|
-
|
|
1325
|
-
// Used internally by decorators:
|
|
1326
|
-
IS_PUBLIC_KEY // 'isPublic' - marks routes as public
|
|
1327
|
-
PERMISSIONS_KEY // 'permissions' - stores permission requirements
|
|
1328
|
-
```
|
|
1329
|
-
|
|
1330
|
-
### Injection Tokens
|
|
1331
|
-
|
|
1332
|
-
```typescript
|
|
1333
|
-
import {
|
|
1334
|
-
CACHE_INSTANCE,
|
|
1335
|
-
PERMISSION_GUARD_CONFIG,
|
|
1336
|
-
LOGGER_INSTANCE,
|
|
1337
|
-
} from '@flusys/nestjs-shared';
|
|
1338
|
-
|
|
1339
|
-
// Injection tokens:
|
|
1340
|
-
CACHE_INSTANCE // 'CACHE_INSTANCE' - HybridCache provider
|
|
1341
|
-
PERMISSION_GUARD_CONFIG // 'PERMISSION_GUARD_CONFIG' - PermissionGuard config
|
|
1342
|
-
LOGGER_INSTANCE // 'LOGGER_INSTANCE' - ILogger provider
|
|
1343
|
-
```
|
|
581
|
+
## Logger System
|
|
1344
582
|
|
|
1345
|
-
|
|
583
|
+
Winston-based structured logger with daily file rotation:
|
|
1346
584
|
|
|
1347
585
|
```typescript
|
|
1348
|
-
import {
|
|
1349
|
-
IDEMPOTENCY_KEY_HEADER,
|
|
1350
|
-
REQUEST_ID_HEADER,
|
|
1351
|
-
CLIENT_TYPE_HEADER,
|
|
1352
|
-
} from '@flusys/nestjs-shared';
|
|
1353
|
-
|
|
1354
|
-
// HTTP header names:
|
|
1355
|
-
IDEMPOTENCY_KEY_HEADER // 'x-idempotency-key' - For IdempotencyInterceptor
|
|
1356
|
-
REQUEST_ID_HEADER // 'x-request-id' - For request correlation
|
|
1357
|
-
CLIENT_TYPE_HEADER // 'x-client-type' - For client detection (browser/api)
|
|
1358
|
-
```
|
|
586
|
+
import { WinstonLoggerAdapter, NestLoggerAdapter } from '@flusys/nestjs-shared/classes';
|
|
1359
587
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
PERMISSIONS_CACHE_PREFIX,
|
|
1365
|
-
IDEMPOTENCY_CACHE_PREFIX,
|
|
1366
|
-
} from '@flusys/nestjs-shared';
|
|
1367
|
-
|
|
1368
|
-
// Cache key prefixes:
|
|
1369
|
-
PERMISSIONS_CACHE_PREFIX // 'permissions' - For user permissions cache
|
|
1370
|
-
IDEMPOTENCY_CACHE_PREFIX // 'idempotency' - For idempotency keys
|
|
588
|
+
// Use as NestJS app logger
|
|
589
|
+
const app = await NestFactory.create(AppModule, {
|
|
590
|
+
logger: new NestLoggerAdapter(),
|
|
591
|
+
});
|
|
1371
592
|
```
|
|
1372
593
|
|
|
1373
|
-
|
|
594
|
+
Log levels: `error`, `warn`, `info`, `http`, `debug`. Configured via `LOG_LEVEL` env var.
|
|
1374
595
|
|
|
1375
|
-
|
|
1376
|
-
import { PERMISSIONS, PermissionCode } from '@flusys/nestjs-shared';
|
|
1377
|
-
|
|
1378
|
-
// Auth Module
|
|
1379
|
-
PERMISSIONS.USER.CREATE // 'user.create'
|
|
1380
|
-
PERMISSIONS.USER.READ // 'user.read'
|
|
1381
|
-
PERMISSIONS.USER.UPDATE // 'user.update'
|
|
1382
|
-
PERMISSIONS.USER.DELETE // 'user.delete'
|
|
1383
|
-
|
|
1384
|
-
PERMISSIONS.COMPANY.CREATE // 'company.create'
|
|
1385
|
-
PERMISSIONS.COMPANY.READ // 'company.read'
|
|
1386
|
-
// ...
|
|
1387
|
-
|
|
1388
|
-
PERMISSIONS.BRANCH.CREATE // 'branch.create'
|
|
1389
|
-
// ...
|
|
1390
|
-
|
|
1391
|
-
// IAM Module
|
|
1392
|
-
PERMISSIONS.ACTION.CREATE // 'action.create'
|
|
1393
|
-
PERMISSIONS.ROLE.CREATE // 'role.create'
|
|
1394
|
-
PERMISSIONS.ROLE_ACTION.READ // 'role-action.read'
|
|
1395
|
-
PERMISSIONS.ROLE_ACTION.ASSIGN // 'role-action.assign'
|
|
1396
|
-
PERMISSIONS.USER_ROLE.READ // 'user-role.read'
|
|
1397
|
-
PERMISSIONS.USER_ROLE.ASSIGN // 'user-role.assign'
|
|
1398
|
-
PERMISSIONS.USER_ACTION.READ // 'user-action.read'
|
|
1399
|
-
PERMISSIONS.USER_ACTION.ASSIGN // 'user-action.assign'
|
|
1400
|
-
PERMISSIONS.COMPANY_ACTION.READ // 'company-action.read'
|
|
1401
|
-
PERMISSIONS.COMPANY_ACTION.ASSIGN // 'company-action.assign'
|
|
1402
|
-
|
|
1403
|
-
// Storage Module
|
|
1404
|
-
PERMISSIONS.FILE.CREATE // 'file.create'
|
|
1405
|
-
PERMISSIONS.FOLDER.CREATE // 'folder.create'
|
|
1406
|
-
PERMISSIONS.STORAGE_CONFIG.CREATE // 'storage-config.create'
|
|
1407
|
-
|
|
1408
|
-
// Email Module
|
|
1409
|
-
PERMISSIONS.EMAIL_CONFIG.CREATE // 'email-config.create'
|
|
1410
|
-
PERMISSIONS.EMAIL_TEMPLATE.CREATE // 'email-template.create'
|
|
1411
|
-
|
|
1412
|
-
// Form Builder Module
|
|
1413
|
-
PERMISSIONS.FORM.CREATE // 'form.create'
|
|
1414
|
-
PERMISSIONS.FORM_RESULT.CREATE // 'form-result.create'
|
|
1415
|
-
|
|
1416
|
-
// Type-safe permission code type:
|
|
1417
|
-
type PermissionCode = 'user.create' | 'user.read' | ... ; // Union of all permission strings
|
|
1418
|
-
```
|
|
596
|
+
Files rotate daily. Configured via `LOG_DIR`, `LOG_MAX_SIZE`, `LOG_MAX_FILES` env vars.
|
|
1419
597
|
|
|
1420
598
|
---
|
|
1421
599
|
|
|
1422
|
-
##
|
|
1423
|
-
|
|
1424
|
-
### Winston Logger
|
|
1425
|
-
|
|
1426
|
-
Production-ready logging with tenant-aware routing and daily rotation.
|
|
1427
|
-
|
|
1428
|
-
**Configuration via environment:**
|
|
1429
|
-
| Variable | Description | Default |
|
|
1430
|
-
|----------|-------------|---------|
|
|
1431
|
-
| `LOG_DIR` | Directory for log files | `logs/` |
|
|
1432
|
-
| `LOG_LEVEL` | Minimum log level | `info` (prod), `debug` (dev) |
|
|
1433
|
-
| `LOG_MAX_SIZE` | Max file size before rotation | `20m` |
|
|
1434
|
-
| `LOG_MAX_FILES` | Max number of log files to keep | `14d` |
|
|
1435
|
-
| `USE_TENANT_MODE` | Enable tenant-aware log routing | `false` |
|
|
1436
|
-
| `DISABLE_HTTP_LOGGING` | Disable HTTP request/response logs | `false` |
|
|
1437
|
-
|
|
1438
|
-
**Disable HTTP Logging:**
|
|
1439
|
-
```bash
|
|
1440
|
-
# .env - Disable middleware HTTP logs (keep service-level @LogAction logs)
|
|
1441
|
-
DISABLE_HTTP_LOGGING=true
|
|
1442
|
-
```
|
|
1443
|
-
|
|
1444
|
-
When disabled, LoggerMiddleware skips request/response logging but preserves:
|
|
1445
|
-
- `@LogAction` decorator logs
|
|
1446
|
-
- Manual `logger.log()` calls
|
|
1447
|
-
- Error logs from GlobalExceptionFilter
|
|
1448
|
-
|
|
1449
|
-
**Log Format (Production):**
|
|
1450
|
-
```
|
|
1451
|
-
────────────────────────────────────────────────────────────────────────────────
|
|
1452
|
-
2024-01-15 10:30:45.123 | Information | [request-uuid]
|
|
1453
|
-
Context: HTTP | Endpoint: POST /api/users | Status: 200 | Duration: 45ms
|
|
1454
|
-
Request finished "HTTP/1.1" "POST" "/api/users" - 200 534 "application/json" 45.00ms
|
|
1455
|
-
Metadata: {"userId":"user-123","tenantId":"tenant-1"}
|
|
1456
|
-
```
|
|
600
|
+
## Permission System
|
|
1457
601
|
|
|
1458
|
-
|
|
602
|
+
Permission logic supports complex AND/OR trees with wildcard matching:
|
|
1459
603
|
|
|
1460
604
|
```typescript
|
|
1461
|
-
import {
|
|
605
|
+
import { IPermissionLogic, IActionNode, IGroupNode } from '@flusys/nestjs-shared/interfaces';
|
|
1462
606
|
|
|
1463
|
-
//
|
|
1464
|
-
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
**Log File Structure:**
|
|
607
|
+
// Single permission
|
|
608
|
+
const simple: IPermissionLogic = { action: 'product.read' };
|
|
1468
609
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
├── combined-2024-01-15.log # All logs (tenant mode off)
|
|
1472
|
-
├── error-2024-01-15.log # Error logs only
|
|
1473
|
-
└── {tenantId}/ # Tenant-specific folders (tenant mode on)
|
|
1474
|
-
└── combined-2024-01-15.log
|
|
1475
|
-
```
|
|
1476
|
-
|
|
1477
|
-
### Logger Adapters
|
|
1478
|
-
|
|
1479
|
-
```typescript
|
|
1480
|
-
import { WinstonLoggerAdapter, NestLoggerAdapter, ILogger } from '@flusys/nestjs-shared';
|
|
1481
|
-
import { Logger } from '@nestjs/common';
|
|
1482
|
-
|
|
1483
|
-
// ILogger interface
|
|
1484
|
-
interface ILogger {
|
|
1485
|
-
log(message: string, context?: string, ...args: any[]): void;
|
|
1486
|
-
error(message: string, trace?: string, context?: string, ...args: any[]): void;
|
|
1487
|
-
warn(message: string, context?: string, ...args: any[]): void;
|
|
1488
|
-
debug(message: string, context?: string, ...args: any[]): void;
|
|
1489
|
-
verbose(message: string, context?: string, ...args: any[]): void;
|
|
1490
|
-
}
|
|
610
|
+
// Wildcard
|
|
611
|
+
const wildcard: IPermissionLogic = { action: 'product.*' };
|
|
1491
612
|
|
|
1492
|
-
//
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
613
|
+
// AND logic (user needs ALL of these)
|
|
614
|
+
const andLogic: IPermissionLogic = {
|
|
615
|
+
operator: 'AND',
|
|
616
|
+
children: [
|
|
617
|
+
{ action: 'product.read' },
|
|
618
|
+
{ action: 'product.update' },
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// OR logic (user needs ANY of these)
|
|
623
|
+
const orLogic: IPermissionLogic = {
|
|
624
|
+
operator: 'OR',
|
|
625
|
+
children: [
|
|
626
|
+
{ action: 'admin.*' },
|
|
627
|
+
{ action: 'product.read' },
|
|
628
|
+
],
|
|
629
|
+
};
|
|
1497
630
|
|
|
1498
|
-
//
|
|
1499
|
-
const
|
|
631
|
+
// Nested
|
|
632
|
+
const nested: IPermissionLogic = {
|
|
633
|
+
operator: 'AND',
|
|
634
|
+
children: [
|
|
635
|
+
{ action: 'product.read' },
|
|
636
|
+
{
|
|
637
|
+
operator: 'OR',
|
|
638
|
+
children: [{ action: 'admin.manage' }, { action: 'product.update' }],
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
};
|
|
1500
642
|
```
|
|
1501
643
|
|
|
1502
644
|
---
|
|
1503
645
|
|
|
1504
|
-
##
|
|
1505
|
-
|
|
1506
|
-
### Main Exports
|
|
646
|
+
## Permission Constants
|
|
1507
647
|
|
|
1508
|
-
All
|
|
648
|
+
All module permission strings are centralized here:
|
|
1509
649
|
|
|
1510
650
|
```typescript
|
|
1511
|
-
// Import everything from main entry
|
|
1512
651
|
import {
|
|
1513
|
-
// Classes
|
|
1514
|
-
ApiService,
|
|
1515
|
-
RequestScopedApiService,
|
|
1516
|
-
createApiController,
|
|
1517
|
-
HybridCache,
|
|
1518
|
-
WinstonLoggerAdapter,
|
|
1519
|
-
NestLoggerAdapter,
|
|
1520
|
-
instance as winstonLogger,
|
|
1521
|
-
|
|
1522
|
-
// Controller Types
|
|
1523
|
-
ApiEndpoint,
|
|
1524
|
-
SecurityLevel,
|
|
1525
|
-
EndpointSecurity,
|
|
1526
|
-
ApiSecurityConfig,
|
|
1527
|
-
ApiControllerOptions,
|
|
1528
|
-
|
|
1529
|
-
// Decorators
|
|
1530
|
-
CurrentUser,
|
|
1531
|
-
Public,
|
|
1532
|
-
RequirePermission,
|
|
1533
|
-
RequireAnyPermission,
|
|
1534
|
-
RequirePermissionLogic,
|
|
1535
|
-
RequirePermissionCondition, // @deprecated - use RequirePermissionLogic
|
|
1536
|
-
SanitizeHtml,
|
|
1537
|
-
SanitizeAndTrim,
|
|
1538
|
-
ApiResponseDto,
|
|
1539
|
-
ArrayResponseType,
|
|
1540
|
-
LogAction,
|
|
1541
|
-
ILogActionOptions,
|
|
1542
|
-
|
|
1543
|
-
// Guards
|
|
1544
|
-
JwtAuthGuard,
|
|
1545
|
-
PermissionGuard,
|
|
1546
|
-
|
|
1547
|
-
// Interceptors
|
|
1548
|
-
ResponseMetaInterceptor,
|
|
1549
|
-
IdempotencyInterceptor,
|
|
1550
|
-
SetCreatedByOnBody,
|
|
1551
|
-
SetUpdateByOnBody,
|
|
1552
|
-
SetDeletedByOnBody,
|
|
1553
|
-
createSetUserFieldInterceptor, // Factory function
|
|
1554
|
-
DeleteEmptyIdFromBodyInterceptor,
|
|
1555
|
-
Slug,
|
|
1556
|
-
|
|
1557
|
-
// Modules
|
|
1558
|
-
CacheModule,
|
|
1559
|
-
DataSourceModule,
|
|
1560
|
-
DataSourceModuleOptions,
|
|
1561
|
-
DataSourceOptionsFactory,
|
|
1562
|
-
DataSourceModuleAsyncOptions,
|
|
1563
|
-
UtilsModule,
|
|
1564
|
-
UtilsService,
|
|
1565
|
-
MultiTenantDataSourceService,
|
|
1566
|
-
|
|
1567
|
-
// DTOs
|
|
1568
|
-
FilterAndPaginationDto,
|
|
1569
|
-
GetByIdBodyDto,
|
|
1570
|
-
PaginationDto,
|
|
1571
|
-
DeleteDto,
|
|
1572
|
-
SingleResponseDto,
|
|
1573
|
-
ListResponseDto,
|
|
1574
|
-
BulkResponseDto,
|
|
1575
|
-
MessageResponseDto,
|
|
1576
|
-
IdentityResponseDto,
|
|
1577
|
-
PaginationMetaDto,
|
|
1578
|
-
BulkMetaDto,
|
|
1579
|
-
RequestMetaDto,
|
|
1580
|
-
|
|
1581
|
-
// Entities
|
|
1582
|
-
Identity,
|
|
1583
|
-
UserRoot,
|
|
1584
|
-
|
|
1585
|
-
// Interfaces
|
|
1586
|
-
ILoggedUserInfo,
|
|
1587
|
-
IService,
|
|
1588
|
-
IDataSourceProvider,
|
|
1589
|
-
IModuleConfigService,
|
|
1590
|
-
ILogger,
|
|
1591
|
-
IIdentity,
|
|
1592
|
-
ILogicNode,
|
|
1593
|
-
IActionNode,
|
|
1594
|
-
IGroupNode,
|
|
1595
|
-
IPermissionLogic,
|
|
1596
|
-
SimplePermissionConfig,
|
|
1597
|
-
PermissionConfig,
|
|
1598
|
-
PermissionGuardConfig,
|
|
1599
|
-
|
|
1600
|
-
// Middleware
|
|
1601
|
-
LoggerMiddleware,
|
|
1602
|
-
IRequestContext,
|
|
1603
|
-
requestContext,
|
|
1604
|
-
getRequestId,
|
|
1605
|
-
getTenantId,
|
|
1606
|
-
getUserId,
|
|
1607
|
-
getCompanyId,
|
|
1608
|
-
|
|
1609
|
-
// Filters
|
|
1610
|
-
GlobalExceptionFilter,
|
|
1611
|
-
|
|
1612
|
-
// Exceptions
|
|
1613
|
-
BaseAppException,
|
|
1614
|
-
IBaseAppExceptionOptions,
|
|
1615
|
-
IValidationError,
|
|
1616
|
-
NotFoundException,
|
|
1617
|
-
ValidationException,
|
|
1618
|
-
UnauthorizedException,
|
|
1619
|
-
ForbiddenException,
|
|
1620
|
-
ConflictException,
|
|
1621
|
-
InternalServerException,
|
|
1622
|
-
ServiceUnavailableException,
|
|
1623
|
-
InsufficientPermissionsException,
|
|
1624
|
-
NoPermissionsFoundException,
|
|
1625
|
-
PermissionSystemUnavailableException,
|
|
1626
|
-
|
|
1627
|
-
// Constants
|
|
1628
|
-
IS_PUBLIC_KEY,
|
|
1629
|
-
PERMISSIONS_KEY,
|
|
1630
|
-
CACHE_INSTANCE,
|
|
1631
|
-
PERMISSION_GUARD_CONFIG,
|
|
1632
|
-
LOGGER_INSTANCE,
|
|
1633
|
-
IDEMPOTENCY_KEY_HEADER,
|
|
1634
|
-
REQUEST_ID_HEADER,
|
|
1635
|
-
CLIENT_TYPE_HEADER,
|
|
1636
|
-
PERMISSIONS_CACHE_PREFIX,
|
|
1637
|
-
IDEMPOTENCY_CACHE_PREFIX,
|
|
1638
|
-
PERMISSIONS,
|
|
1639
|
-
PermissionCode,
|
|
1640
|
-
// Individual permission exports
|
|
1641
652
|
USER_PERMISSIONS,
|
|
1642
653
|
COMPANY_PERMISSIONS,
|
|
1643
|
-
BRANCH_PERMISSIONS,
|
|
1644
|
-
ACTION_PERMISSIONS,
|
|
1645
654
|
ROLE_PERMISSIONS,
|
|
1646
|
-
|
|
1647
|
-
USER_ROLE_PERMISSIONS,
|
|
1648
|
-
USER_ACTION_PERMISSIONS,
|
|
1649
|
-
COMPANY_ACTION_PERMISSIONS,
|
|
655
|
+
ACTION_PERMISSIONS,
|
|
1650
656
|
FILE_PERMISSIONS,
|
|
1651
657
|
FOLDER_PERMISSIONS,
|
|
1652
658
|
STORAGE_CONFIG_PERMISSIONS,
|
|
1653
|
-
EMAIL_CONFIG_PERMISSIONS,
|
|
1654
|
-
EMAIL_TEMPLATE_PERMISSIONS,
|
|
1655
659
|
FORM_PERMISSIONS,
|
|
1656
660
|
FORM_RESULT_PERMISSIONS,
|
|
661
|
+
EMAIL_TEMPLATE_PERMISSIONS,
|
|
662
|
+
EMAIL_CONFIG_PERMISSIONS,
|
|
663
|
+
LANGUAGE_PERMISSIONS,
|
|
664
|
+
TRANSLATION_PERMISSIONS,
|
|
665
|
+
NOTIFICATION_PERMISSIONS,
|
|
666
|
+
EVENT_PERMISSIONS,
|
|
667
|
+
} from '@flusys/nestjs-shared/constants';
|
|
1657
668
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
escapeHtmlVariables,
|
|
1661
|
-
applyCompanyFilter,
|
|
1662
|
-
buildCompanyWhereCondition,
|
|
1663
|
-
hasCompanyId,
|
|
1664
|
-
validateCompanyOwnership,
|
|
1665
|
-
ICompanyFilterConfig,
|
|
1666
|
-
ICompanyEnabled,
|
|
1667
|
-
isBrowserRequest,
|
|
1668
|
-
buildCookieOptions,
|
|
1669
|
-
parseDurationToMs,
|
|
1670
|
-
generateSlug,
|
|
1671
|
-
generateUniqueSlug,
|
|
1672
|
-
} from '@flusys/nestjs-shared';
|
|
669
|
+
// Example: USER_PERMISSIONS
|
|
670
|
+
// { CREATE: 'user.create', READ: 'user.read', UPDATE: 'user.update', DELETE: 'user.delete' }
|
|
1673
671
|
```
|
|
1674
672
|
|
|
1675
673
|
---
|
|
1676
674
|
|
|
1677
|
-
##
|
|
675
|
+
## Cross-Module Adapter Interfaces
|
|
1678
676
|
|
|
1679
|
-
|
|
677
|
+
These interfaces are defined in `nestjs-shared` so modules can reference the contract without importing each other:
|
|
1680
678
|
|
|
1681
679
|
```typescript
|
|
1682
|
-
import {
|
|
1683
|
-
|
|
1684
|
-
@Injectable()
|
|
1685
|
-
export class MyService {
|
|
1686
|
-
constructor(
|
|
1687
|
-
@Inject(UtilsService) private readonly utilsService: UtilsService,
|
|
1688
|
-
@Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
|
|
1689
|
-
) {}
|
|
1690
|
-
|
|
1691
|
-
// Cache key generation with optional tenant prefix
|
|
1692
|
-
getCacheKey(entityName: string, params: any, entityId?: string, tenantId?: string): string;
|
|
1693
|
-
// Example: this.utilsService.getCacheKey('user', { filter: {} }, 'user-123', 'tenant-1')
|
|
1694
|
-
// Returns: 'tenant_tenant-1_entity_user_id_user-123_select_{"filter":{}}'
|
|
1695
|
-
|
|
1696
|
-
// Track cache keys for invalidation
|
|
1697
|
-
async trackCacheKey(
|
|
1698
|
-
cacheKey: string,
|
|
1699
|
-
entityName: string,
|
|
1700
|
-
cacheManager: HybridCache,
|
|
1701
|
-
entityId?: string,
|
|
1702
|
-
tenantId?: string,
|
|
1703
|
-
): Promise<void>;
|
|
1704
|
-
|
|
1705
|
-
// Clear all cached entries for an entity
|
|
1706
|
-
async clearCache(
|
|
1707
|
-
entityName: string,
|
|
1708
|
-
cacheManager: HybridCache,
|
|
1709
|
-
entityId?: string,
|
|
1710
|
-
tenantId?: string,
|
|
1711
|
-
): Promise<void>;
|
|
1712
|
-
|
|
1713
|
-
// Generate URL-friendly slug
|
|
1714
|
-
transformToSlug(value: string, salt?: boolean): string;
|
|
1715
|
-
// Example: this.utilsService.transformToSlug('My Product Name')
|
|
1716
|
-
// Returns: 'my-product-name'
|
|
1717
|
-
// With salt: 'my-product-name-42'
|
|
1718
|
-
|
|
1719
|
-
// Generate random integer
|
|
1720
|
-
getRandomInt(min: number, max: number): number;
|
|
1721
|
-
}
|
|
680
|
+
import { INotificationAdapter, NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
|
|
681
|
+
import { IEventManagerAdapter, EVENT_MANAGER_ADAPTER } from '@flusys/nestjs-shared/interfaces';
|
|
1722
682
|
```
|
|
1723
683
|
|
|
684
|
+
Pattern: Feature module A wants to send a notification (defined in module B). Instead of importing B from A (circular dep), A imports only the interface from `nestjs-shared` and uses `@Optional() @Inject(NOTIFICATION_ADAPTER)`.
|
|
685
|
+
|
|
1724
686
|
---
|
|
1725
687
|
|
|
1726
|
-
##
|
|
688
|
+
## Troubleshooting
|
|
689
|
+
|
|
690
|
+
**`Cannot read properties of undefined` on injected service**
|
|
1727
691
|
|
|
1728
|
-
|
|
692
|
+
All constructor dependencies need `@Inject()` decorators — esbuild bundling loses TypeScript metadata:
|
|
1729
693
|
|
|
1730
694
|
```typescript
|
|
1731
|
-
//
|
|
1732
|
-
|
|
1733
|
-
export class ProductService extends ApiService<
|
|
1734
|
-
CreateProductDto,
|
|
1735
|
-
UpdateProductDto,
|
|
1736
|
-
IProduct,
|
|
1737
|
-
Product,
|
|
1738
|
-
Repository<Product>
|
|
1739
|
-
> {
|
|
1740
|
-
// Override hooks for custom business logic
|
|
1741
|
-
protected async beforeInsertOperation(dto, user, qr): Promise<void> {
|
|
1742
|
-
// Validation, transformations, etc.
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
695
|
+
// Wrong
|
|
696
|
+
constructor(private readonly service: MyService) {}
|
|
1745
697
|
|
|
1746
|
-
//
|
|
1747
|
-
@
|
|
1748
|
-
export class RoleService extends RequestScopedApiService<...> {
|
|
1749
|
-
protected resolveEntity() {
|
|
1750
|
-
return this.config.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
698
|
+
// Correct
|
|
699
|
+
constructor(@Inject(MyService) private readonly service: MyService) {}
|
|
1753
700
|
```
|
|
1754
701
|
|
|
1755
|
-
|
|
702
|
+
---
|
|
1756
703
|
|
|
1757
|
-
|
|
704
|
+
**`No metadata for entity X`**
|
|
1758
705
|
|
|
1759
|
-
|
|
1760
|
-
// CORRECT - explicit injection
|
|
1761
|
-
constructor(
|
|
1762
|
-
@Inject(MyService) private readonly myService: MyService,
|
|
1763
|
-
@Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
|
|
1764
|
-
@Inject(UtilsService) private readonly utils: UtilsService,
|
|
1765
|
-
) {}
|
|
1766
|
-
|
|
1767
|
-
// WRONG - may fail in bundled code
|
|
1768
|
-
constructor(private readonly myService: MyService) {}
|
|
1769
|
-
```
|
|
1770
|
-
|
|
1771
|
-
### 3. Use Decorators Consistently
|
|
706
|
+
You are using `RequestScopedApiService` but forgot to call `ensureRepositoryInitialized()` before accessing `this.repository`. Override the method:
|
|
1772
707
|
|
|
1773
708
|
```typescript
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
@RequirePermission('user.create', 'admin') // Multiple permissions (AND)
|
|
1781
|
-
@RequireAnyPermission('user.read', 'admin') // Multiple permissions (OR)
|
|
1782
|
-
@RequirePermissionLogic({ type: 'group', ... }) // Complex logic
|
|
1783
|
-
|
|
1784
|
-
// Mark public routes sparingly - security risk!
|
|
1785
|
-
@Public()
|
|
1786
|
-
|
|
1787
|
-
// Avoid direct request access - use decorators
|
|
1788
|
-
// @Req() req: Request // Not type-safe, avoid!
|
|
709
|
+
protected override async ensureRepositoryInitialized(): Promise<void> {
|
|
710
|
+
if (!this.repositoryInitialized) {
|
|
711
|
+
this.repository = await this.dataSourceProvider.getRepository(MyEntity);
|
|
712
|
+
this.repositoryInitialized = true;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
1789
715
|
```
|
|
1790
716
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
```typescript
|
|
1794
|
-
// GOOD - centralized security config
|
|
1795
|
-
export class UserController extends createApiController(
|
|
1796
|
-
CreateUserDto, UpdateUserDto, UserResponseDto,
|
|
1797
|
-
{
|
|
1798
|
-
security: {
|
|
1799
|
-
getAll: 'jwt',
|
|
1800
|
-
insert: { level: 'permission', permissions: ['user.create'] },
|
|
1801
|
-
update: { level: 'permission', permissions: ['user.update'] },
|
|
1802
|
-
delete: { level: 'permission', permissions: ['user.delete'] },
|
|
1803
|
-
},
|
|
1804
|
-
},
|
|
1805
|
-
) {}
|
|
1806
|
-
|
|
1807
|
-
// AVOID - scattered guards on each endpoint
|
|
1808
|
-
@UseGuards(JwtGuard)
|
|
1809
|
-
@Post('create')
|
|
1810
|
-
create() {}
|
|
1811
|
-
```
|
|
717
|
+
---
|
|
1812
718
|
|
|
1813
|
-
|
|
719
|
+
**Cache not connecting to Redis**
|
|
1814
720
|
|
|
1815
|
-
|
|
1816
|
-
import { PERMISSIONS } from '@flusys/nestjs-shared';
|
|
721
|
+
`CacheModule` falls back to in-memory automatically if `REDIS_URL` is not set or Redis is unreachable. Check that `REDIS_URL` is set correctly in your `.env` file.
|
|
1817
722
|
|
|
1818
|
-
|
|
1819
|
-
@RequirePermission(PERMISSIONS.USER.CREATE)
|
|
723
|
+
---
|
|
1820
724
|
|
|
1821
|
-
|
|
1822
|
-
@RequirePermission('user.create')
|
|
1823
|
-
```
|
|
725
|
+
**`instanceof` checks fail after bundling**
|
|
1824
726
|
|
|
1825
|
-
|
|
727
|
+
Use property-based checks instead:
|
|
1826
728
|
|
|
1827
729
|
```typescript
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
// In service getExtraManipulateQuery hook
|
|
1831
|
-
protected async getExtraManipulateQuery(query, dto, user) {
|
|
1832
|
-
applyCompanyFilter(query, {
|
|
1833
|
-
isCompanyFeatureEnabled: this.config.isCompanyFeatureEnabled(),
|
|
1834
|
-
entityAlias: this.entityName,
|
|
1835
|
-
}, user);
|
|
1836
|
-
return { query, isRaw: false };
|
|
1837
|
-
}
|
|
730
|
+
// Wrong (fails after esbuild)
|
|
731
|
+
if (dto instanceof UpdateProductDto) { }
|
|
1838
732
|
|
|
1839
|
-
//
|
|
1840
|
-
|
|
733
|
+
// Correct
|
|
734
|
+
if ('id' in dto && dto.id) { }
|
|
1841
735
|
```
|
|
1842
736
|
|
|
1843
|
-
|
|
737
|
+
---
|
|
1844
738
|
|
|
1845
|
-
|
|
1846
|
-
import { LogAction } from '@flusys/nestjs-shared';
|
|
739
|
+
## License
|
|
1847
740
|
|
|
1848
|
-
|
|
1849
|
-
@LogAction({ action: 'user.create', includeParams: false })
|
|
1850
|
-
async createUser(dto: CreateUserDto): Promise<User> {
|
|
1851
|
-
return this.repository.save(dto);
|
|
1852
|
-
// Errors automatically logged with stack trace, then re-thrown
|
|
1853
|
-
}
|
|
1854
|
-
```
|
|
741
|
+
MIT © FLUSYS
|
|
1855
742
|
|
|
1856
743
|
---
|
|
1857
744
|
|
|
1858
|
-
**
|
|
745
|
+
> Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
|