@flusys/nestjs-shared 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +96 -606
  2. package/cjs/classes/api-controller.class.js +81 -10
  3. package/cjs/classes/api-service.class.js +9 -7
  4. package/cjs/classes/index.js +0 -1
  5. package/cjs/constants/index.js +0 -4
  6. package/cjs/constants/message-keys.js +3 -21
  7. package/cjs/constants/permissions.js +0 -10
  8. package/cjs/decorators/api-response.decorator.js +1 -2
  9. package/cjs/decorators/index.js +0 -1
  10. package/cjs/decorators/require-permission.decorator.js +0 -4
  11. package/cjs/exceptions/base-app.exception.js +4 -4
  12. package/cjs/exceptions/permission.exception.js +1 -1
  13. package/cjs/filters/global-exception.filter.js +4 -4
  14. package/cjs/interfaces/index.js +0 -1
  15. package/cjs/modules/datasource/multi-tenant-datasource.service.js +2 -2
  16. package/cjs/utils/date-time.util.js +94 -0
  17. package/cjs/utils/index.js +1 -0
  18. package/cjs/utils/query-helpers.util.js +2 -2
  19. package/cjs/utils/request.util.js +25 -0
  20. package/classes/index.d.ts +0 -1
  21. package/constants/index.d.ts +0 -1
  22. package/constants/message-keys.d.ts +2 -44
  23. package/constants/permissions.d.ts +0 -12
  24. package/decorators/api-response.decorator.d.ts +1 -1
  25. package/decorators/index.d.ts +0 -1
  26. package/decorators/require-permission.decorator.d.ts +0 -1
  27. package/exceptions/base-app.exception.d.ts +2 -2
  28. package/fesm/classes/api-controller.class.js +82 -11
  29. package/fesm/classes/api-service.class.js +10 -8
  30. package/fesm/classes/index.js +0 -1
  31. package/fesm/constants/index.js +0 -1
  32. package/fesm/constants/message-keys.js +3 -19
  33. package/fesm/constants/permissions.js +0 -7
  34. package/fesm/decorators/api-response.decorator.js +2 -20
  35. package/fesm/decorators/index.js +0 -1
  36. package/fesm/decorators/require-permission.decorator.js +0 -1
  37. package/fesm/exceptions/base-app.exception.js +4 -4
  38. package/fesm/exceptions/permission.exception.js +1 -1
  39. package/fesm/filters/global-exception.filter.js +4 -4
  40. package/fesm/interfaces/index.js +0 -1
  41. package/fesm/modules/datasource/multi-tenant-datasource.service.js +2 -2
  42. package/fesm/utils/date-time.util.js +70 -0
  43. package/fesm/utils/index.js +1 -0
  44. package/fesm/utils/query-helpers.util.js +2 -2
  45. package/fesm/utils/request.util.js +22 -0
  46. package/interfaces/event-manager-adapter.interface.d.ts +12 -12
  47. package/interfaces/index.d.ts +0 -1
  48. package/package.json +2 -2
  49. package/utils/date-time.util.d.ts +8 -0
  50. package/utils/index.d.ts +1 -0
  51. package/utils/request.util.d.ts +2 -0
  52. package/cjs/classes/winston-logger-adapter.class.js +0 -99
  53. package/cjs/decorators/sanitize-html.decorator.js +0 -36
  54. package/cjs/interfaces/logger.interface.js +0 -4
  55. package/classes/winston-logger-adapter.class.d.ts +0 -23
  56. package/decorators/sanitize-html.decorator.d.ts +0 -2
  57. package/fesm/classes/winston-logger-adapter.class.js +0 -81
  58. package/fesm/decorators/sanitize-html.decorator.js +0 -45
  59. package/fesm/interfaces/logger.interface.js +0 -1
  60. package/interfaces/logger.interface.d.ts +0 -7
package/README.md CHANGED
@@ -1,92 +1,9 @@
1
1
  # @flusys/nestjs-shared
2
2
 
3
- > Shared NestJS infrastructuregeneric CRUD base classes, JWT guards, permission system, hybrid caching, structured logging, multi-tenancy, and response standardization for the entire FLUSYS ecosystem.
3
+ Shared infrastructure for all FLUSYS NestJS packagesbase CRUD classes, JWT guard, permission guard, response DTOs, hybrid cache, structured logging, and soft-delete entities.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-shared.svg)](https://www.npmjs.com/package/@flusys/nestjs-shared)
6
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
- ---
12
-
13
- ## Table of Contents
14
-
15
- - [Overview](#overview)
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)
27
- - [Guards](#guards)
28
- - [Decorators](#decorators)
29
- - [Interceptors](#interceptors)
30
- - [HybridCache](#hybridcache)
31
- - [Middlewares](#middlewares)
32
- - [Exceptions & Filters](#exceptions--filters)
33
- - [Modules](#modules)
34
- - [Multi-Tenant DataSource Service](#multi-tenant-datasource-service)
35
- - [Logger System](#logger-system)
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)
41
-
42
- ---
43
-
44
- ## Overview
45
-
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.
47
-
48
- ---
49
-
50
- ## Features
51
-
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
61
-
62
- ---
63
-
64
- ## Architecture Position
65
-
66
- ```
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)
74
- ```
75
-
76
- ---
77
-
78
- ## Compatibility
79
-
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` |
90
7
 
91
8
  ---
92
9
 
@@ -94,22 +11,24 @@ auth iam storage form-builder email event-manager notification localizati
94
11
 
95
12
  ```bash
96
13
  npm install @flusys/nestjs-shared @flusys/nestjs-core
14
+ npm install typeorm @nestjs/passport @nestjs/jwt passport-jwt winston
97
15
  ```
98
16
 
99
- ---
100
-
101
- ## Quick Start
17
+ ## 1. Module Registration
102
18
 
103
- ### 1. Register Global Modules
19
+ Register once in your root `AppModule`:
104
20
 
105
21
  ```typescript
106
22
  import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
107
- import { CacheModule, UtilsModule, LoggerMiddleware } from '@flusys/nestjs-shared';
23
+ import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
24
+ import { LoggerMiddleware } from '@flusys/nestjs-shared/middlewares';
25
+ import { GlobalExceptionFilter } from '@flusys/nestjs-shared/filters';
26
+ import { ResponseMetaInterceptor } from '@flusys/nestjs-shared/interceptors';
108
27
 
109
28
  @Module({
110
29
  imports: [
111
- CacheModule.forRoot(true), // true = global
112
- UtilsModule,
30
+ CacheModule.forRoot(true), // true = global; provides 'CACHE_INSTANCE' token
31
+ UtilsModule, // provides UtilsService
113
32
  ],
114
33
  })
115
34
  export class AppModule implements NestModule {
@@ -117,107 +36,69 @@ export class AppModule implements NestModule {
117
36
  consumer.apply(LoggerMiddleware).forRoutes('*');
118
37
  }
119
38
  }
120
- ```
121
39
 
122
- ### 2. Register Global Exception Filter & Interceptors
123
-
124
- ```typescript
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);
132
- }
40
+ // In bootstrap()
41
+ app.useGlobalFilters(new GlobalExceptionFilter());
42
+ app.useGlobalInterceptors(new ResponseMetaInterceptor());
133
43
  ```
134
44
 
135
- ---
136
-
137
- ## Base Classes
138
-
139
- ### ApiService
45
+ ## 2. Entity Base Class
140
46
 
141
- Generic CRUD service for simple, static repositories (no dynamic entity switching):
47
+ Every entity must extend `Identity`:
142
48
 
143
49
  ```typescript
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';
50
+ import { Entity, Column } from 'typeorm';
51
+ import { Identity } from '@flusys/nestjs-shared/entities';
150
52
 
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);
165
- }
53
+ @Entity('products')
54
+ export class Product extends Identity {
55
+ @Column({ length: 255 })
56
+ name: string;
57
+ // inherited: id (uuid), createdAt, updatedAt, deletedAt,
58
+ // createdById, updatedById, deletedById
166
59
  }
167
60
  ```
168
61
 
169
- **Built-in methods:**
62
+ ## 3. Generic CRUD — `createApiController` + `RequestScopedApiService`
170
63
 
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
182
-
183
- For services that need **dynamic entity loading** per request (DataSource Provider pattern). Use this for all feature modules that have `WithCompany` entity variants:
64
+ ### Service (REQUEST-scoped, DataSource Provider pattern)
184
65
 
185
66
  ```typescript
186
67
  import { Injectable, Scope, Inject } from '@nestjs/common';
68
+ import { EntityTarget, Repository } from 'typeorm';
187
69
  import { RequestScopedApiService } from '@flusys/nestjs-shared/classes';
188
70
  import { HybridCache } from '@flusys/nestjs-shared/classes';
189
71
  import { UtilsService } from '@flusys/nestjs-shared/modules';
190
72
 
191
- @Injectable({ scope: Scope.REQUEST }) // REQUEST scope required
73
+ @Injectable({ scope: Scope.REQUEST }) // required
192
74
  export class ProductService extends RequestScopedApiService<
193
75
  CreateProductDto,
194
76
  UpdateProductDto,
195
77
  IProduct,
196
- ProductBase,
197
- Repository<ProductBase>
78
+ Product,
79
+ Repository<Product>
198
80
  > {
199
81
  constructor(
200
82
  @Inject('CACHE_INSTANCE') protected override cacheManager: HybridCache,
201
83
  @Inject(UtilsService) protected override utilsService: UtilsService,
202
- @Inject(ModuleConfigService) private readonly moduleConfig: ModuleConfigService,
203
- @Inject(DataSourceProvider) private readonly dataSourceProvider: DataSourceProvider,
84
+ @Inject(ProductConfigService) private readonly config: ProductConfigService,
85
+ @Inject(ProductDataSourceProvider) private readonly dsp: ProductDataSourceProvider,
204
86
  ) {
205
87
  super('product', null as any, cacheManager, utilsService, ProductService.name, true);
206
88
  }
207
89
 
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
- }
90
+ // Required: tell the base class which entity and which datasource to use
91
+ protected resolveEntity(): EntityTarget<Product> {
92
+ return this.config.isCompanyFeatureEnabled() ? ProductWithCompany : Product;
93
+ }
94
+
95
+ protected getDataSourceProvider() {
96
+ return this.dsp;
214
97
  }
215
98
  }
216
99
  ```
217
100
 
218
- ### createApiController
219
-
220
- Factory function that generates a fully-typed CRUD controller class:
101
+ ### Controller (factory function)
221
102
 
222
103
  ```typescript
223
104
  import { Controller, Inject } from '@nestjs/common';
@@ -235,11 +116,14 @@ export class ProductController extends createApiController<
235
116
  security: {
236
117
  insert: { level: 'permission', permissions: ['product.create'] },
237
118
  insertMany: { level: 'permission', permissions: ['product.create'] },
238
- getById: { level: 'permission', permissions: ['product.read'] },
239
119
  getAll: { level: 'permission', permissions: ['product.read'] },
120
+ getById: { level: 'permission', permissions: ['product.read'] },
240
121
  update: { level: 'permission', permissions: ['product.update'] },
241
122
  updateMany: { level: 'permission', permissions: ['product.update'] },
242
123
  delete: { level: 'permission', permissions: ['product.delete'] },
124
+ bulkUpsert: { level: 'permission', permissions: ['product.create'] },
125
+ getByIds: { level: 'permission', permissions: ['product.read'] },
126
+ getByFilter:{ level: 'permission', permissions: ['product.read'] },
243
127
  },
244
128
  }) {
245
129
  constructor(@Inject(ProductService) public override service: ProductService) {
@@ -248,236 +132,54 @@ export class ProductController extends createApiController<
248
132
  }
249
133
  ```
250
134
 
251
- **Generated endpoints:**
135
+ Generated POST-only endpoints: `/insert`, `/insert-many`, `/get-all`, `/get/:id`,
136
+ `/get-by-ids`, `/get-by-filter`, `/update`, `/update-many`, `/bulk-upsert`, `/delete`.
252
137
 
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 |
262
-
263
- **Security levels:**
264
-
265
- | Level | Description |
266
- |-------|-------------|
267
- | `'public'` | No authentication required (`@Public()`) |
268
- | `'authenticated'` | Requires valid JWT only |
269
- | `'permission'` | Requires JWT + specific action permissions |
270
-
271
- ---
138
+ Security levels: `'public'` (no auth), `'jwt'` (token only), `'permission'` (token + action check).
272
139
 
273
- ## Base Entities
274
-
275
- ### Identity (IdentityEntity)
276
-
277
- Base entity for all FLUSYS entities. All entities must extend this:
278
-
279
- ```typescript
280
- import { Identity } from '@flusys/nestjs-shared/entities';
281
-
282
- @Entity('products')
283
- export class Product extends Identity {
284
- @Column() name: string;
285
- // id, createdAt, updatedAt, deletedAt inherited
286
- }
287
- ```
288
-
289
- **Fields:**
290
-
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 |
299
-
300
- ### UserRoot
301
-
302
- Extended base entity for user entities. `@flusys/nestjs-auth`'s `User` extends this:
303
-
304
- ```typescript
305
- import { UserRoot } from '@flusys/nestjs-shared/entities';
306
-
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
312
- }
313
- ```
314
-
315
- ---
316
-
317
- ## Response DTOs
318
-
319
- All responses follow a consistent structure:
320
-
321
- ```typescript
322
- import {
323
- SingleResponseDto,
324
- ListResponseDto,
325
- BulkResponseDto,
326
- MessageResponseDto,
327
- } from '@flusys/nestjs-shared/dtos';
328
- ```
329
-
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 |
336
-
337
- **PaginationMetaDto:**
338
- ```typescript
339
- { total: number; page: number; pageSize: number; count: number; hasMore?: boolean; totalPages?: number }
340
- ```
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
-
344
- ---
345
-
346
- ## Guards
347
-
348
- ### JwtAuthGuard
349
-
350
- Validates Bearer JWT tokens on incoming requests:
140
+ ## 4. Guards and Decorators
351
141
 
352
142
  ```typescript
353
143
  import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
354
-
355
- @UseGuards(JwtAuthGuard)
356
- @Post('protected')
357
- async protectedEndpoint(@CurrentUser() user: ILoggedUserInfo) { }
358
- ```
359
-
360
- Applied globally by default in the FLUSYS app. Use `@Public()` to bypass.
361
-
362
- ### PermissionGuard
363
-
364
- Checks that the authenticated user has the required action permission:
365
-
366
- ```typescript
367
144
  import { PermissionGuard } from '@flusys/nestjs-shared/guards';
368
- import { RequirePermission } from '@flusys/nestjs-shared/decorators';
145
+ import {
146
+ CurrentUser,
147
+ Public,
148
+ RequirePermission,
149
+ RequireAnyPermission,
150
+ } from '@flusys/nestjs-shared/decorators';
151
+ import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
369
152
 
370
153
  @UseGuards(JwtAuthGuard, PermissionGuard)
371
154
  @RequirePermission('product.create')
372
155
  @Post('insert')
373
- async insert() { }
374
- ```
375
-
376
- > **Note:** Always use `@Inject(Reflector)` in your guard constructors — esbuild bundling loses TypeScript metadata.
377
-
378
- ---
379
-
380
- ## Decorators
381
-
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 |
393
-
394
- ### ILoggedUserInfo
395
-
396
- ```typescript
397
- interface ILoggedUserInfo {
398
- id: string;
399
- email: string;
400
- name: string;
401
- companyId?: string;
402
- branchId?: string;
403
- permissions?: string[];
404
- isSystemUser?: boolean;
405
- }
406
- ```
407
-
408
- ---
409
-
410
- ## Interceptors
411
-
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 |
420
-
421
- ---
422
-
423
- ## HybridCache
424
-
425
- Two-tier cache: L1 in-memory (CacheableMemory) → L2 Redis. Automatically falls back to in-memory if Redis is unavailable.
426
-
427
- ```typescript
428
- import { HybridCache } from '@flusys/nestjs-shared/classes';
429
-
430
- @Injectable()
431
- export class MyService {
432
- constructor(@Inject('CACHE_INSTANCE') private cache: HybridCache) {}
433
-
434
- async getData(key: string): Promise<any> {
435
- const cached = await this.cache.get(key);
436
- if (cached) return cached;
437
-
438
- const data = await this.fetchFromDb();
439
- await this.cache.set(key, data, 3600); // TTL in seconds
440
- return data;
441
- }
156
+ async insert(@Body() dto: CreateProductDto, @CurrentUser() user: ILoggedUserInfo) { }
442
157
 
443
- async invalidate(key: string): Promise<void> {
444
- await this.cache.delete(key);
445
- }
446
-
447
- async invalidateByPrefix(prefix: string): Promise<void> {
448
- await this.cache.deleteByPrefix(prefix);
449
- }
450
- }
158
+ // Mark endpoint public (no JWT required)
159
+ @Public()
160
+ @Post('public-list')
161
+ async publicList() { }
451
162
  ```
452
163
 
453
- ---
454
-
455
- ## Middlewares
164
+ `ILoggedUserInfo` fields: `id`, `email`, `name?`, `phone?`, `profilePictureId?`, `companyId?`, `branchId?`.
456
165
 
457
- ### LoggerMiddleware
166
+ ## 5. Response DTOs
458
167
 
459
- HTTP request/response logger with correlation IDs. Attach to all routes:
168
+ All controllers return one of these shapes:
460
169
 
461
170
  ```typescript
462
- import { LoggerMiddleware } from '@flusys/nestjs-shared/middlewares';
171
+ import {
172
+ SingleResponseDto, // insert, update, getById
173
+ ListResponseDto, // getAll, getByIds
174
+ BulkResponseDto, // insertMany, updateMany, bulkUpsert
175
+ MessageResponseDto, // delete
176
+ } from '@flusys/nestjs-shared/dtos';
463
177
 
464
- @Module({})
465
- export class AppModule implements NestModule {
466
- configure(consumer: MiddlewareConsumer) {
467
- consumer.apply(LoggerMiddleware).forRoutes('*');
468
- }
469
- }
178
+ // All shapes include: success, message, messageKey (for i18n), _meta (requestId, timestamp, duration)
179
+ // ListResponseDto / BulkResponseDto also include: meta (total, page, pageSize, count, totalPages)
470
180
  ```
471
181
 
472
- Logged fields: method, URL, status, duration, requestId, userId (from JWT), IP, user-agent.
473
-
474
- The middleware also sets an `AsyncLocalStorage` context so `requestId` and `userId` are available in any service without passing them through method parameters.
475
-
476
- ---
477
-
478
- ## Exceptions & Filters
479
-
480
- ### Built-in Exception Classes
182
+ ## 6. Exceptions
481
183
 
482
184
  ```typescript
483
185
  import {
@@ -485,261 +187,49 @@ import {
485
187
  ConflictException,
486
188
  UnauthorizedException,
487
189
  ForbiddenException,
488
- BadRequestException,
489
190
  ValidationException,
191
+ InternalServerException,
490
192
  } from '@flusys/nestjs-shared/exceptions';
491
193
 
492
- // All exceptions accept { message, messageKey } for localization
493
- throw new NotFoundException({ message: 'User not found', messageKey: 'user.not_found' });
494
- ```
495
-
496
- ### GlobalExceptionFilter
497
-
498
- Catches all unhandled exceptions and returns a consistent error response:
499
-
500
- ```typescript
501
- // Register globally
502
- app.useGlobalFilters(new GlobalExceptionFilter());
503
- ```
504
-
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": "..." }
513
- }
514
- ```
515
-
516
- ---
517
-
518
- ## Modules
519
-
520
- ### CacheModule
521
-
522
- ```typescript
523
- import { CacheModule } from '@flusys/nestjs-shared/modules';
524
-
525
- // Global (register once in AppModule)
526
- CacheModule.forRoot(true)
527
-
528
- // Local (non-global)
529
- CacheModule.forRoot(false)
530
- ```
531
-
532
- Provides `HybridCache` as `'CACHE_INSTANCE'` injection token. Automatically connects to Redis if `REDIS_URL` is set.
533
-
534
- ### UtilsModule
535
-
536
- ```typescript
537
- import { UtilsModule } from '@flusys/nestjs-shared/modules';
538
-
539
- @Module({ imports: [UtilsModule] })
540
- export class AppModule {}
541
- ```
542
-
543
- Provides `UtilsService` with slug generation, HTML sanitization, and query helper utilities.
544
-
545
- ### DataSourceModule
546
-
547
- ```typescript
548
- import { DataSourceModule } from '@flusys/nestjs-shared/modules';
549
-
550
- DataSourceModule.forRoot(dataSourceOptions)
194
+ throw new NotFoundException({
195
+ message: 'User not found',
196
+ messageKey: USER_MESSAGES.NOT_FOUND,
197
+ });
198
+ throw new ConflictException({
199
+ message: `User already assigned to this ${typeName}. Please refresh and try again.`,
200
+ messageKey: USER_PERMISSION_MESSAGES.ALREADY_ASSIGNED,
201
+ messageVariables: { type: typeName },
202
+ });
203
+ throw new ValidationException([{ field: 'email', message: 'Invalid format' }]);
551
204
  ```
552
205
 
553
- Wraps `MultiTenantDataSourceService` for dynamic tenant-based DataSource resolution.
554
-
555
- ---
556
-
557
- ## Multi-Tenant DataSource Service
206
+ All exceptions produce: `{ success: false, message, messageKey,messageVariables:{} code, _meta }`.
558
207
 
559
- `MultiTenantDataSourceService` manages separate TypeORM DataSource connections per tenant. Each feature module extends it to create an isolated static connection cache:
208
+ ## 7. Hybrid Cache
560
209
 
561
210
  ```typescript
562
- import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
211
+ import { HybridCache } from '@flusys/nestjs-shared/classes';
563
212
 
564
- // Feature module DataSource Provider (extends with isolated cache)
565
213
  @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();
214
+ export class MyService {
215
+ constructor(@Inject('CACHE_INSTANCE') private cache: HybridCache) {}
570
216
 
571
- protected override getSingleDataSource(): DataSource | null {
572
- return ProductDataSourceProvider.moduleDataSource;
217
+ async getData(key: string) {
218
+ const cached = await this.cache.get(key);
219
+ if (cached) return cached;
220
+ const data = await this.fetchFromDb();
221
+ await this.cache.set(key, data, 3600); // TTL in seconds
222
+ return data;
573
223
  }
574
224
 
575
- // ... override other cache accessors
225
+ async invalidate(key: string) { await this.cache.delete(key); }
226
+ async invalidatePrefix(prefix: string) { await this.cache.deleteByPrefix(prefix); }
576
227
  }
577
228
  ```
578
229
 
579
- ---
580
-
581
- ## Logger System
230
+ `CacheModule.forRoot(true)` connects to Redis automatically when `REDIS_URL` is set; otherwise uses in-memory only.
582
231
 
583
- Winston-based structured logger with daily file rotation:
584
-
585
- ```typescript
586
- import { WinstonLoggerAdapter, NestLoggerAdapter } from '@flusys/nestjs-shared/classes';
587
-
588
- // Use as NestJS app logger
589
- const app = await NestFactory.create(AppModule, {
590
- logger: new NestLoggerAdapter(),
591
- });
592
- ```
593
-
594
- Log levels: `error`, `warn`, `info`, `http`, `debug`. Configured via `LOG_LEVEL` env var.
595
-
596
- Files rotate daily. Configured via `LOG_DIR`, `LOG_MAX_SIZE`, `LOG_MAX_FILES` env vars.
597
-
598
- ---
599
-
600
- ## Permission System
601
-
602
- Permission logic supports complex AND/OR trees with wildcard matching:
603
-
604
- ```typescript
605
- import { IPermissionLogic, IActionNode, IGroupNode } from '@flusys/nestjs-shared/interfaces';
606
-
607
- // Single permission
608
- const simple: IPermissionLogic = { action: 'product.read' };
609
-
610
- // Wildcard
611
- const wildcard: IPermissionLogic = { action: 'product.*' };
612
-
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
- };
630
-
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
- };
642
- ```
643
-
644
- ---
645
-
646
- ## Permission Constants
647
-
648
- All module permission strings are centralized here:
649
-
650
- ```typescript
651
- import {
652
- USER_PERMISSIONS,
653
- COMPANY_PERMISSIONS,
654
- ROLE_PERMISSIONS,
655
- ACTION_PERMISSIONS,
656
- FILE_PERMISSIONS,
657
- FOLDER_PERMISSIONS,
658
- STORAGE_CONFIG_PERMISSIONS,
659
- FORM_PERMISSIONS,
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';
668
-
669
- // Example: USER_PERMISSIONS
670
- // { CREATE: 'user.create', READ: 'user.read', UPDATE: 'user.update', DELETE: 'user.delete' }
671
- ```
672
-
673
- ---
674
-
675
- ## Cross-Module Adapter Interfaces
676
-
677
- These interfaces are defined in `nestjs-shared` so modules can reference the contract without importing each other:
678
-
679
- ```typescript
680
- import { INotificationAdapter, NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
681
- import { IEventManagerAdapter, EVENT_MANAGER_ADAPTER } from '@flusys/nestjs-shared/interfaces';
682
- ```
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
-
686
- ---
687
-
688
- ## Troubleshooting
689
-
690
- **`Cannot read properties of undefined` on injected service**
691
-
692
- All constructor dependencies need `@Inject()` decorators — esbuild bundling loses TypeScript metadata:
693
-
694
- ```typescript
695
- // Wrong
696
- constructor(private readonly service: MyService) {}
697
-
698
- // Correct
699
- constructor(@Inject(MyService) private readonly service: MyService) {}
700
- ```
701
-
702
- ---
703
-
704
- **`No metadata for entity X`**
705
-
706
- You are using `RequestScopedApiService` but forgot to call `ensureRepositoryInitialized()` before accessing `this.repository`. Override the method:
707
-
708
- ```typescript
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
- }
715
- ```
716
-
717
- ---
718
-
719
- **Cache not connecting to Redis**
720
-
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.
722
-
723
- ---
724
-
725
- **`instanceof` checks fail after bundling**
726
-
727
- Use property-based checks instead:
728
-
729
- ```typescript
730
- // Wrong (fails after esbuild)
731
- if (dto instanceof UpdateProductDto) { }
732
-
733
- // Correct
734
- if ('id' in dto && dto.id) { }
735
- ```
736
-
737
- ---
738
232
 
739
233
  ## License
740
234
 
741
235
  MIT © FLUSYS
742
-
743
- ---
744
-
745
- > Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.