@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 CHANGED
@@ -1,1858 +1,745 @@
1
- # Shared Package Guide
1
+ # @flusys/nestjs-shared
2
2
 
3
- > **Package:** `@flusys/nestjs-shared`
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
- This comprehensive guide covers the shared package - the shared NestJS infrastructure layer.
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-shared.svg)](https://www.npmjs.com/package/@flusys/nestjs-shared)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![NestJS](https://img.shields.io/badge/NestJS-11.x-red.svg)](https://nestjs.com/)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.x-green.svg)](https://nodejs.org/)
10
+
11
+ ---
8
12
 
9
13
  ## Table of Contents
10
14
 
11
15
  - [Overview](#overview)
12
- - [Package Architecture](#package-architecture)
13
- - [ApiService - Generic CRUD Service](#apiservice---generic-crud-service)
14
- - [RequestScopedApiService](#requestscopedapiservice)
15
- - [ApiController - Generic CRUD Controller](#apicontroller---generic-crud-controller)
16
- - [Decorators](#decorators)
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
- - [Middleware](#middleware)
28
+ - [Decorators](#decorators)
19
29
  - [Interceptors](#interceptors)
20
- - [Caching System](#caching-system)
21
- - [Multi-Tenant DataSource](#multi-tenant-datasource)
22
- - [DTOs](#dtos)
23
- - [Base Entities](#base-entities)
24
- - [Utilities](#utilities)
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
- - [API Reference](#api-reference)
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` provides shared utilities for building scalable NestJS applications:
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
- - **Generic CRUD** - Standardized API controller and service patterns
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
- ### Package Hierarchy
50
+ ## Features
47
51
 
48
- ```
49
- @flusys/nestjs-core <- Pure TypeScript (foundation)
50
- |
51
- @flusys/nestjs-shared <- Shared NestJS utilities (THIS PACKAGE)
52
- |
53
- @flusys/nestjs-auth <- Uses common classes
54
- |
55
- @flusys/nestjs-iam <- Uses common patterns
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
- ## Package Architecture
64
+ ## Architecture Position
63
65
 
64
66
  ```
65
- nestjs-shared/
66
- ├── src/
67
- ├── classes/ # Base classes
68
- │ │ ├── api-service.class.ts # Generic CRUD service
69
- │ │ ├── api-controller.class.ts # Generic CRUD controller factory (createApiController)
70
- │ │ ├── request-scoped-api.service.ts # REQUEST-scoped service base
71
- │ │ ├── hybrid-cache.class.ts # Two-tier caching (memory + Redis)
72
- │ │ ├── winston.logger.class.ts # Winston logger config with tenant routing
73
- │ │ └── winston-logger-adapter.class.ts # ILogger adapters
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
- ## ApiService - Generic CRUD Service
78
+ ## Compatibility
146
79
 
147
- The `ApiService` base class provides standardized CRUD operations with caching, pagination, and transaction support.
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
- ### Basic Usage
91
+ ---
150
92
 
151
- ```typescript
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
- @Injectable()
160
- export class UserService extends ApiService<
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
- ## RequestScopedApiService
101
+ ## Quick Start
269
102
 
270
- For dynamic entity resolution based on runtime configuration (e.g., company feature toggling). Extends `ApiService` with lazy repository initialization.
103
+ ### 1. Register Global Modules
271
104
 
272
105
  ```typescript
273
- import { RequestScopedApiService, HybridCache, CACHE_INSTANCE } from '@flusys/nestjs-shared';
274
- import { UtilsService, IDataSourceProvider } from '@flusys/nestjs-shared';
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
- constructor(
289
- @Inject(CACHE_INSTANCE) protected override cacheManager: HybridCache,
290
- @Inject(UtilsService) protected override utilsService: UtilsService,
291
- @Inject(ModuleConfigService) private readonly config: ModuleConfigService,
292
- @Inject(DataSourceProvider) private readonly provider: DataSourceProvider,
293
- ) {
294
- // Pass null for repository - will be initialized dynamically
295
- super('role', null as any, cacheManager, utilsService, 'RoleService', true);
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
- ### Key Methods
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
- interface IDataSourceProvider {
332
- getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>>;
333
- getDataSource(): Promise<DataSource>;
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
- ## ApiController - Generic CRUD Controller
137
+ ## Base Classes
340
138
 
341
- The `createApiController` factory creates standardized POST-only RPC controllers with full Swagger documentation.
139
+ ### ApiService
342
140
 
343
- ### Basic Usage
141
+ Generic CRUD service for simple, static repositories (no dynamic entity switching):
344
142
 
345
143
  ```typescript
346
- import { createApiController } from '@flusys/nestjs-shared';
347
- import { Controller, Inject } from '@nestjs/common';
348
- import { ApiTags } from '@nestjs/swagger';
349
-
350
- @ApiTags('Users')
351
- @Controller('users')
352
- export class UserController extends createApiController<
353
- CreateUserDto,
354
- UpdateUserDto,
355
- UserResponseDto,
356
- IUser,
357
- UserService
358
- >(CreateUserDto, UpdateUserDto, UserResponseDto) {
359
- constructor(@Inject(UserService) protected service: UserService) {
360
- super(service);
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
- ### Generated Endpoints
169
+ **Built-in methods:**
366
170
 
367
- | Endpoint | Method | Description | Interceptors |
368
- |----------|--------|-------------|--------------|
369
- | `/insert` | POST | Create entity | Idempotency, SetCreatedByOnBody, Slug |
370
- | `/insert-many` | POST | Create multiple entities | Idempotency, SetCreatedByOnBody, Slug |
371
- | `/get/:id` | POST | Get entity by ID | - |
372
- | `/get-all` | POST | Get paginated list with filters | - |
373
- | `/update` | POST | Update entity | SetUpdateByOnBody, Slug |
374
- | `/update-many` | POST | Update multiple entities | SetUpdateByOnBody, Slug |
375
- | `/delete` | POST | Delete/restore/permanent delete | SetDeletedByOnBody |
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
- ### Security Configuration Types
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
- // Security levels
381
- type SecurityLevel = 'public' | 'jwt' | 'permission';
382
-
383
- // Endpoint names
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
- // Per-endpoint or global config
395
- type ApiSecurityConfig = { [K in ApiEndpoint]?: EndpointSecurity | SecurityLevel };
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
- interface ApiControllerOptions {
398
- security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
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
- ### Security Configuration Examples
218
+ ### createApiController
403
219
 
404
- ```typescript
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
- // Permission with OR logic
432
- {
433
- security: {
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
- // Complex permission logic
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
- delete: {
442
- level: 'permission',
443
- logic: {
444
- type: 'group',
445
- operator: 'AND',
446
- children: [
447
- { type: 'action', actionId: 'user.delete' },
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
- **Important:** When per-endpoint security is specified, unconfigured endpoints default to `'jwt'` (not `'public'`) for security.
251
+ **Generated endpoints:**
460
252
 
461
- ---
462
-
463
- ## Decorators
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
- ### @CurrentUser
263
+ **Security levels:**
466
264
 
467
- Extract the logged-in user from request:
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
- ```typescript
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
- // Extract specific property
481
- @Post('id')
482
- getUserId(@CurrentUser('id') userId: string) {
483
- return { userId };
484
- }
485
- }
486
- ```
273
+ ## Base Entities
487
274
 
488
- ### @Public
275
+ ### Identity (IdentityEntity)
489
276
 
490
- Mark route as public (skip authentication). **Use sparingly - security risk**.
277
+ Base entity for all FLUSYS entities. All entities must extend this:
491
278
 
492
279
  ```typescript
493
- import { Public } from '@flusys/nestjs-shared';
280
+ import { Identity } from '@flusys/nestjs-shared/entities';
494
281
 
495
- @Controller('auth')
496
- export class AuthController {
497
- @Public()
498
- @Post('login')
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
- ### @RequirePermission
289
+ **Fields:**
504
290
 
505
- Require specific permission(s) - **AND logic** by default:
506
-
507
- ```typescript
508
- import { RequirePermission } from '@flusys/nestjs-shared';
509
-
510
- @Controller('admin')
511
- export class AdminController {
512
- // Requires 'admin.dashboard' permission
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
- ### @RequireAnyPermission
300
+ ### UserRoot
525
301
 
526
- Require any of the listed permissions - **OR logic**:
302
+ Extended base entity for user entities. `@flusys/nestjs-auth`'s `User` extends this:
527
303
 
528
304
  ```typescript
529
- import { RequireAnyPermission } from '@flusys/nestjs-shared';
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
- Build complex permission logic with ILogicNode tree (matches frontend ILogicNode):
543
-
544
- ```typescript
545
- import { RequirePermissionLogic } from '@flusys/nestjs-shared';
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
- ### @SanitizeHtml / @SanitizeAndTrim
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
- ### @LogAction
317
+ ## Response DTOs
585
318
 
586
- Method-level logging decorator with automatic correlation context and error handling:
319
+ All responses follow a consistent structure:
587
320
 
588
321
  ```typescript
589
- import { LogAction } from '@flusys/nestjs-shared';
590
-
591
- @Injectable()
592
- export class UserService {
593
- // Module name for routing logs to module-specific files (production)
594
- protected readonly moduleName = 'auth';
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
- **Features:**
617
- - Automatic start/end logging with duration
618
- - Error logging with stack trace (then re-throws)
619
- - Sensitive data redaction (password, secret, token, apiKey, authorization)
620
- - Request context (requestId, userId, tenantId, companyId)
621
- - Module-specific log routing (production mode)
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
- import { ApiResponseDto } from '@flusys/nestjs-shared';
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 for protected routes. Extends Passport's `AuthGuard('jwt')` and respects `@Public()` decorator.
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
- // Apply globally
655
- @Module({
656
- providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
657
- })
658
- export class AppModule {}
355
+ @UseGuards(JwtAuthGuard)
356
+ @Post('protected')
357
+ async protectedEndpoint(@CurrentUser() user: ILoggedUserInfo) { }
659
358
  ```
660
359
 
661
- **Important:** Constructor needs `@Inject(Reflector)` for bundled code (this is already handled in the guard).
360
+ Applied globally by default in the FLUSYS app. Use `@Public()` to bypass.
662
361
 
663
362
  ### PermissionGuard
664
363
 
665
- Checks user permissions from cache with AND/OR/nested logic support and wildcard matching.
364
+ Checks that the authenticated user has the required action permission:
666
365
 
667
366
  ```typescript
668
- import { PermissionGuard, PERMISSION_GUARD_CONFIG, CACHE_INSTANCE } from '@flusys/nestjs-shared';
367
+ import { PermissionGuard } from '@flusys/nestjs-shared/guards';
368
+ import { RequirePermission } from '@flusys/nestjs-shared/decorators';
669
369
 
670
- @Module({
671
- providers: [
672
- { provide: APP_GUARD, useClass: PermissionGuard },
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
- ### Permission Exceptions
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
- ## Middleware
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
- **Configuration via environment:**
785
- - `LOG_LEVEL` - Logging level (debug enables verbose logging)
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
- **Usage:**
394
+ ### ILoggedUserInfo
788
395
 
789
396
  ```typescript
790
- import { LoggerMiddleware } from '@flusys/nestjs-shared';
791
-
792
- @Module({})
793
- export class AppModule implements NestModule {
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
- startTime: number;
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
- ### ResponseMetaInterceptor
841
-
842
- Automatically adds `_meta` to all responses with `success` field:
843
-
844
- ```typescript
845
- // Response structure:
846
- {
847
- "success": true,
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
- ## Caching System
423
+ ## HybridCache
935
424
 
936
- ### HybridCache
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); // TTL in seconds
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.del(key);
443
+ async invalidate(key: string): Promise<void> {
444
+ await this.cache.delete(key);
959
445
  }
960
446
 
961
- async invalidateAll() {
962
- await this.cache.reset(); // Clear L1
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
- ## Multi-Tenant DataSource
455
+ ## Middlewares
993
456
 
994
- Dynamic database connection management with connection pooling.
457
+ ### LoggerMiddleware
995
458
 
996
- ### Setup
459
+ HTTP request/response logger with correlation IDs. Attach to all routes:
997
460
 
998
461
  ```typescript
999
- import { DataSourceModule } from '@flusys/nestjs-shared';
462
+ import { LoggerMiddleware } from '@flusys/nestjs-shared/middlewares';
1000
463
 
1001
- @Module({
1002
- imports: [
1003
- DataSourceModule.forRoot({
1004
- bootstrapAppConfig: { databaseMode: 'multi-tenant' },
1005
- defaultDatabaseConfig: {
1006
- type: 'mysql',
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
- ### MultiTenantDataSourceService
472
+ Logged fields: method, URL, status, duration, requestId, userId (from JWT), IP, user-agent.
1023
473
 
1024
- | Method | Description |
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
- ## DTOs
478
+ ## Exceptions & Filters
1038
479
 
1039
- ### FilterAndPaginationDto
480
+ ### Built-in Exception Classes
1040
481
 
1041
482
  ```typescript
1042
- import { FilterAndPaginationDto } from '@flusys/nestjs-shared';
483
+ import {
484
+ NotFoundException,
485
+ ConflictException,
486
+ UnauthorizedException,
487
+ ForbiddenException,
488
+ BadRequestException,
489
+ ValidationException,
490
+ } from '@flusys/nestjs-shared/exceptions';
1043
491
 
1044
- // Request body
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
- ### DeleteDto
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
- ### Response DTOs
498
+ Catches all unhandled exceptions and returns a consistent error response:
1073
499
 
1074
500
  ```typescript
1075
- // Single item
1076
- class SingleResponseDto<T> {
1077
- success: boolean;
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
- // Message only
1102
- class MessageResponseDto {
1103
- success: boolean;
1104
- message: string;
1105
- _meta?: RequestMetaDto;
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
- ## Base Entities
1112
-
1113
- ### Identity Entity
518
+ ## Modules
1114
519
 
1115
- Base entity with UUID and timestamps:
520
+ ### CacheModule
1116
521
 
1117
522
  ```typescript
1118
- import { Identity } from '@flusys/nestjs-shared';
523
+ import { CacheModule } from '@flusys/nestjs-shared/modules';
1119
524
 
1120
- @Entity()
1121
- export class Product extends Identity {
1122
- // Inherited: id, createdAt, updatedAt, deletedAt
1123
- // Inherited: createdById, updatedById, deletedById
525
+ // Global (register once in AppModule)
526
+ CacheModule.forRoot(true)
1124
527
 
1125
- @Column()
1126
- name: string;
1127
- }
528
+ // Local (non-global)
529
+ CacheModule.forRoot(false)
1128
530
  ```
1129
531
 
1130
- ### UserRoot Entity
532
+ Provides `HybridCache` as `'CACHE_INSTANCE'` injection token. Automatically connects to Redis if `REDIS_URL` is set.
1131
533
 
1132
- Base user entity with common fields:
534
+ ### UtilsModule
1133
535
 
1134
536
  ```typescript
1135
- import { UserRoot } from '@flusys/nestjs-shared';
537
+ import { UtilsModule } from '@flusys/nestjs-shared/modules';
1136
538
 
1137
- @Entity()
1138
- export class User extends UserRoot {
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
- ## Utilities
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
- ```typescript
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
- ### Request Utilities
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: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
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
- ## Error Handling
557
+ ## Multi-Tenant DataSource Service
1228
558
 
1229
- ### BaseAppException
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
- throw new ValidationException([
1263
- { field: 'email', message: 'Invalid format' },
1264
- { field: 'password', message: 'Too short' },
1265
- ]);
1266
-
1267
- throw new UnauthorizedException('Invalid credentials');
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
- **Response format (all errors):**
1294
- ```json
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
- ## Constants
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
- ### Header Names
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
- ### Cache Key Prefixes
1361
-
1362
- ```typescript
1363
- import {
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
- ### Permission Constants
594
+ Log levels: `error`, `warn`, `info`, `http`, `debug`. Configured via `LOG_LEVEL` env var.
1374
595
 
1375
- ```typescript
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
- ## Logger System
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
- **Logger Modes:**
602
+ Permission logic supports complex AND/OR trees with wildcard matching:
1459
603
 
1460
604
  ```typescript
1461
- import { instance as winstonLogger } from '@flusys/nestjs-shared';
605
+ import { IPermissionLogic, IActionNode, IGroupNode } from '@flusys/nestjs-shared/interfaces';
1462
606
 
1463
- // Development: Console output with colors
1464
- // Production: File-based with daily rotation + tenant routing
1465
- ```
1466
-
1467
- **Log File Structure:**
607
+ // Single permission
608
+ const simple: IPermissionLogic = { action: 'product.read' };
1468
609
 
1469
- ```
1470
- logs/
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
- // Winston adapter (recommended for production)
1493
- // Automatically includes correlation context (requestId, userId, tenantId, companyId)
1494
- const logger = new WinstonLoggerAdapter('MyService');
1495
- logger.log('Operation completed', undefined, { orderId: '123' });
1496
- // Output: { requestId: 'uuid', userId: 'user-id', context: 'MyService', message: 'Operation completed', orderId: '123' }
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
- // NestJS adapter (for testing or simple use cases)
1499
- const nestLogger = new NestLoggerAdapter(new Logger('MyService'));
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
- ## API Reference
1505
-
1506
- ### Main Exports
646
+ ## Permission Constants
1507
647
 
1508
- All exports are available from the main package entry point:
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
- ROLE_ACTION_PERMISSIONS,
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
- // Utilities
1659
- escapeHtml,
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
- ## UtilsService
675
+ ## Cross-Module Adapter Interfaces
1678
676
 
1679
- Global utility service for cache management and string operations.
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 { UtilsService, CACHE_INSTANCE, HybridCache } from '@flusys/nestjs-shared';
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
- ## Best Practices
688
+ ## Troubleshooting
689
+
690
+ **`Cannot read properties of undefined` on injected service**
1727
691
 
1728
- ### 1. Use Generic Service Pattern
692
+ All constructor dependencies need `@Inject()` decorators — esbuild bundling loses TypeScript metadata:
1729
693
 
1730
694
  ```typescript
1731
- // Extend ApiService for consistent CRUD operations
1732
- @Injectable()
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
- // For dynamic entity resolution, use RequestScopedApiService
1747
- @Injectable({ scope: Scope.REQUEST })
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
- ### 2. Always Use @Inject() Decorators
702
+ ---
1756
703
 
1757
- Required for esbuild bundled code (NestJS DI metadata may be lost):
704
+ **`No metadata for entity X`**
1758
705
 
1759
- ```typescript
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
- // Use built-in decorators for type safety
1775
- @CurrentUser() user: ILoggedUserInfo
1776
- @CurrentUser('id') userId: string // Extract specific property
1777
-
1778
- // Permission decorators
1779
- @RequirePermission('user.create') // Single permission (AND)
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
- ### 4. Configure Security at Controller Level
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
- ### 5. Use Permission Constants
719
+ **Cache not connecting to Redis**
1814
720
 
1815
- ```typescript
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
- // GOOD - use constants for type safety and refactoring
1819
- @RequirePermission(PERMISSIONS.USER.CREATE)
723
+ ---
1820
724
 
1821
- // AVOID - hardcoded strings prone to typos
1822
- @RequirePermission('user.create')
1823
- ```
725
+ **`instanceof` checks fail after bundling**
1824
726
 
1825
- ### 6. Leverage Company Filtering Utilities
727
+ Use property-based checks instead:
1826
728
 
1827
729
  ```typescript
1828
- import { applyCompanyFilter, validateCompanyOwnership } from '@flusys/nestjs-shared';
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
- // Validate ownership before operations
1840
- validateCompanyOwnership(entity, user, this.config.isCompanyFeatureEnabled(), 'Product');
733
+ // Correct
734
+ if ('id' in dto && dto.id) { }
1841
735
  ```
1842
736
 
1843
- ### 7. Error Handling Pattern
737
+ ---
1844
738
 
1845
- ```typescript
1846
- import { LogAction } from '@flusys/nestjs-shared';
739
+ ## License
1847
740
 
1848
- // Use @LogAction for consistent logging with sensitive data redaction
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
- **Last Updated:** 2026-02-25
745
+ > Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.