@flusys/nestjs-shared 4.1.1 → 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.
- package/README.md +96 -606
- package/cjs/classes/api-controller.class.js +81 -10
- package/cjs/classes/api-service.class.js +8 -6
- package/cjs/classes/index.js +0 -1
- package/cjs/constants/index.js +0 -4
- package/cjs/constants/message-keys.js +3 -21
- package/cjs/constants/permissions.js +0 -10
- package/cjs/decorators/api-response.decorator.js +1 -2
- package/cjs/decorators/index.js +0 -1
- package/cjs/decorators/require-permission.decorator.js +0 -4
- package/cjs/exceptions/base-app.exception.js +4 -4
- package/cjs/exceptions/permission.exception.js +1 -1
- package/cjs/filters/global-exception.filter.js +4 -4
- package/cjs/interfaces/index.js +0 -1
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +2 -2
- package/cjs/utils/date-time.util.js +94 -0
- package/cjs/utils/index.js +1 -0
- package/cjs/utils/query-helpers.util.js +2 -2
- package/cjs/utils/request.util.js +25 -0
- package/classes/index.d.ts +0 -1
- package/constants/index.d.ts +0 -1
- package/constants/message-keys.d.ts +2 -44
- package/constants/permissions.d.ts +0 -12
- package/decorators/api-response.decorator.d.ts +1 -1
- package/decorators/index.d.ts +0 -1
- package/decorators/require-permission.decorator.d.ts +0 -1
- package/exceptions/base-app.exception.d.ts +2 -2
- package/fesm/classes/api-controller.class.js +82 -11
- package/fesm/classes/api-service.class.js +9 -7
- package/fesm/classes/index.js +0 -1
- package/fesm/constants/index.js +0 -1
- package/fesm/constants/message-keys.js +3 -19
- package/fesm/constants/permissions.js +0 -7
- package/fesm/decorators/api-response.decorator.js +2 -20
- package/fesm/decorators/index.js +0 -1
- package/fesm/decorators/require-permission.decorator.js +0 -1
- package/fesm/exceptions/base-app.exception.js +4 -4
- package/fesm/exceptions/permission.exception.js +1 -1
- package/fesm/filters/global-exception.filter.js +4 -4
- package/fesm/interfaces/index.js +0 -1
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +2 -2
- package/fesm/utils/date-time.util.js +70 -0
- package/fesm/utils/index.js +1 -0
- package/fesm/utils/query-helpers.util.js +2 -2
- package/fesm/utils/request.util.js +22 -0
- package/interfaces/event-manager-adapter.interface.d.ts +12 -12
- package/interfaces/index.d.ts +0 -1
- package/package.json +2 -2
- package/utils/date-time.util.d.ts +8 -0
- package/utils/index.d.ts +1 -0
- package/utils/request.util.d.ts +2 -0
- package/cjs/classes/winston-logger-adapter.class.js +0 -99
- package/cjs/decorators/sanitize-html.decorator.js +0 -36
- package/cjs/interfaces/logger.interface.js +0 -4
- package/classes/winston-logger-adapter.class.d.ts +0 -23
- package/decorators/sanitize-html.decorator.d.ts +0 -2
- package/fesm/classes/winston-logger-adapter.class.js +0 -81
- package/fesm/decorators/sanitize-html.decorator.js +0 -45
- package/fesm/interfaces/logger.interface.js +0 -1
- package/interfaces/logger.interface.d.ts +0 -7
package/README.md
CHANGED
|
@@ -1,92 +1,9 @@
|
|
|
1
1
|
# @flusys/nestjs-shared
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared infrastructure for all FLUSYS NestJS packages — base CRUD classes, JWT guard, permission guard, response DTOs, hybrid cache, structured logging, and soft-delete entities.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@flusys/nestjs-shared)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nestjs.com/)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](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
|
-
|
|
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
|
|
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),
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
47
|
+
Every entity must extend `Identity`:
|
|
142
48
|
|
|
143
49
|
```typescript
|
|
144
|
-
import {
|
|
145
|
-
import {
|
|
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
|
-
@
|
|
152
|
-
export class
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
62
|
+
## 3. Generic CRUD — `createApiController` + `RequestScopedApiService`
|
|
170
63
|
|
|
171
|
-
|
|
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 }) //
|
|
73
|
+
@Injectable({ scope: Scope.REQUEST }) // required
|
|
192
74
|
export class ProductService extends RequestScopedApiService<
|
|
193
75
|
CreateProductDto,
|
|
194
76
|
UpdateProductDto,
|
|
195
77
|
IProduct,
|
|
196
|
-
|
|
197
|
-
Repository<
|
|
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(
|
|
203
|
-
@Inject(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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 {
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
166
|
+
## 5. Response DTOs
|
|
458
167
|
|
|
459
|
-
|
|
168
|
+
All controllers return one of these shapes:
|
|
460
169
|
|
|
461
170
|
```typescript
|
|
462
|
-
import {
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
---
|
|
556
|
-
|
|
557
|
-
## Multi-Tenant DataSource Service
|
|
206
|
+
All exceptions produce: `{ success: false, message, messageKey,messageVariables:{} code, _meta }`.
|
|
558
207
|
|
|
559
|
-
|
|
208
|
+
## 7. Hybrid Cache
|
|
560
209
|
|
|
561
210
|
```typescript
|
|
562
|
-
import {
|
|
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
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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.
|