@flusys/nestjs-shared 0.1.0-beta.3 → 1.0.0-rc
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 +106 -138
- package/cjs/classes/api-controller.class.js +9 -45
- package/cjs/classes/api-service.class.js +4 -41
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/request-scoped-api.service.js +4 -53
- package/cjs/classes/winston.logger.class.js +5 -15
- package/cjs/constants/index.js +16 -11
- package/cjs/constants/permissions.js +174 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +35 -121
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +39 -94
- package/cjs/interceptors/index.js +1 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +2 -30
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +43 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/logged-user-info.interface.js +1 -2
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/interfaces/permission.interface.js +1 -10
- package/cjs/middlewares/logger.middleware.js +2 -6
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +31 -111
- package/cjs/modules/utils/utils.service.js +63 -145
- package/cjs/utils/error-handler.util.js +91 -13
- package/cjs/utils/html-sanitizer.util.js +74 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +5 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +167 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -7
- package/fesm/classes/api-controller.class.js +10 -93
- package/fesm/classes/api-service.class.js +5 -46
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/request-scoped-api.service.js +4 -53
- package/fesm/classes/winston.logger.class.js +6 -18
- package/fesm/constants/index.js +16 -29
- package/fesm/constants/permissions.js +121 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +39 -142
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +39 -94
- package/fesm/interceptors/index.js +1 -0
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +4 -30
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +36 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/logged-user-info.interface.js +1 -2
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/interfaces/permission.interface.js +0 -12
- package/fesm/middlewares/logger.middleware.js +2 -6
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +31 -111
- package/fesm/modules/utils/utils.service.js +50 -143
- package/fesm/utils/error-handler.util.js +93 -14
- package/fesm/utils/html-sanitizer.util.js +82 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/interceptors/index.d.ts +1 -0
- package/interceptors/set-create-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +1 -5
- package/interceptors/set-user-field-on-body.interceptor.d.ts +2 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +10 -4
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +23 -13
- package/utils/html-sanitizer.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interfaces/base-query.interface.d.ts +0 -7
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ This comprehensive guide covers the shared package - the shared NestJS infrastru
|
|
|
32
32
|
- **Generic CRUD** - Standardized API controller and service patterns
|
|
33
33
|
- **Permission System** - Role and permission-based access control
|
|
34
34
|
- **Caching** - In-memory + Redis hybrid caching
|
|
35
|
-
- **Request Correlation** - AsyncLocalStorage-based request tracking
|
|
36
|
-
- **Middleware** - Logging, correlation, and performance monitoring
|
|
35
|
+
- **Request Correlation** - AsyncLocalStorage-based request tracking
|
|
36
|
+
- **Middleware** - Logging, correlation, and performance monitoring
|
|
37
37
|
- **Interceptors** - Response metadata, idempotency, auto field setting
|
|
38
38
|
- **Multi-Tenancy** - Dynamic database connection management
|
|
39
39
|
- **Error Handling** - Centralized error handling utilities
|
|
@@ -121,7 +121,6 @@ nestjs-shared/
|
|
|
121
121
|
│ │
|
|
122
122
|
│ ├── interfaces/ # TypeScript interfaces
|
|
123
123
|
│ │ ├── api.interface.ts
|
|
124
|
-
│ │ ├── base-query.interface.ts
|
|
125
124
|
│ │ ├── identity.interface.ts
|
|
126
125
|
│ │ ├── logged-user-info.interface.ts
|
|
127
126
|
│ │ ├── logger.interface.ts
|
|
@@ -298,7 +297,7 @@ override async getExtraManipulateQuery(
|
|
|
298
297
|
|
|
299
298
|
## ApiController - Generic CRUD Controller
|
|
300
299
|
|
|
301
|
-
The `createApiController` factory creates standardized
|
|
300
|
+
The `createApiController` factory creates standardized POST-only RPC controllers.
|
|
302
301
|
|
|
303
302
|
### Basic Usage
|
|
304
303
|
|
|
@@ -462,23 +461,52 @@ export class ReportsController {
|
|
|
462
461
|
}
|
|
463
462
|
```
|
|
464
463
|
|
|
465
|
-
### @
|
|
464
|
+
### @RequirePermissionCondition
|
|
466
465
|
|
|
467
|
-
|
|
466
|
+
Build complex permission conditions with nested AND/OR logic:
|
|
468
467
|
|
|
469
468
|
```typescript
|
|
470
|
-
import {
|
|
469
|
+
import { RequirePermissionCondition } from '@flusys/nestjs-shared/decorators';
|
|
471
470
|
|
|
472
471
|
@Controller('sensitive')
|
|
473
472
|
export class SensitiveController {
|
|
474
|
-
|
|
475
|
-
@
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
473
|
+
// Simple: User needs 'admin' OR 'manager'
|
|
474
|
+
@RequirePermissionCondition({
|
|
475
|
+
operator: 'or',
|
|
476
|
+
permissions: ['admin', 'manager'],
|
|
477
|
+
})
|
|
478
|
+
@Get('simple')
|
|
479
|
+
getSimpleData() {}
|
|
480
|
+
|
|
481
|
+
// Complex: User needs 'users.read' AND ('admin' OR 'manager')
|
|
482
|
+
@RequirePermissionCondition({
|
|
483
|
+
operator: 'and',
|
|
484
|
+
permissions: ['users.read'],
|
|
485
|
+
children: [
|
|
486
|
+
{ operator: 'or', permissions: ['admin', 'manager'] }
|
|
487
|
+
]
|
|
488
|
+
})
|
|
489
|
+
@Get('complex')
|
|
490
|
+
getComplexData() {}
|
|
491
|
+
|
|
492
|
+
// Very complex: (A AND B) OR (C AND D)
|
|
493
|
+
@RequirePermissionCondition({
|
|
494
|
+
operator: 'or',
|
|
495
|
+
children: [
|
|
496
|
+
{ operator: 'and', permissions: ['finance.read', 'finance.write'] },
|
|
497
|
+
{ operator: 'and', permissions: ['admin.full', 'reports.access'] }
|
|
498
|
+
]
|
|
499
|
+
})
|
|
500
|
+
@Get('very-complex')
|
|
501
|
+
getVeryComplexData() {}
|
|
479
502
|
}
|
|
480
503
|
```
|
|
481
504
|
|
|
505
|
+
**Note:** For simple "require ALL permissions" use case, use `@RequirePermission` which defaults to AND logic:
|
|
506
|
+
```typescript
|
|
507
|
+
@RequirePermission('admin.access', 'security.clearance') // User needs BOTH
|
|
508
|
+
```
|
|
509
|
+
|
|
482
510
|
---
|
|
483
511
|
|
|
484
512
|
## Guards
|
|
@@ -487,106 +515,62 @@ The shared package provides two guards for authentication and authorization:
|
|
|
487
515
|
|
|
488
516
|
### JwtAuthGuard
|
|
489
517
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
The `JwtAuthGuard` validates JWT tokens for protected routes. It extends Passport's `AuthGuard('jwt')` and respects the `@Public()` decorator.
|
|
493
|
-
|
|
494
|
-
**Why in shared module:**
|
|
495
|
-
- All feature modules (auth, iam, storage) need JWT authentication
|
|
496
|
-
- Keeps feature modules independent (no cross-dependencies)
|
|
497
|
-
- JwtStrategy registration remains in auth module
|
|
498
|
-
- This guard just validates tokens using the registered strategy
|
|
499
|
-
|
|
500
|
-
**Usage:**
|
|
518
|
+
Validates JWT tokens for protected routes. Extends Passport's `AuthGuard('jwt')` and respects `@Public()` decorator.
|
|
501
519
|
|
|
502
520
|
```typescript
|
|
503
521
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
504
|
-
import { UseGuards } from '@nestjs/common';
|
|
505
522
|
|
|
506
|
-
//
|
|
523
|
+
// Apply globally
|
|
524
|
+
@Module({
|
|
525
|
+
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
|
|
526
|
+
})
|
|
527
|
+
export class AppModule {}
|
|
528
|
+
|
|
529
|
+
// Or per controller
|
|
507
530
|
@Controller('users')
|
|
508
531
|
@UseGuards(JwtAuthGuard)
|
|
509
532
|
export class UserController {
|
|
510
|
-
// All routes protected
|
|
511
|
-
|
|
512
533
|
@Post('login')
|
|
513
|
-
@Public() // Skip JWT check
|
|
534
|
+
@Public() // Skip JWT check
|
|
514
535
|
async login() { }
|
|
515
536
|
}
|
|
516
|
-
|
|
517
|
-
// Or apply globally
|
|
518
|
-
@Module({
|
|
519
|
-
providers: [
|
|
520
|
-
{
|
|
521
|
-
provide: APP_GUARD,
|
|
522
|
-
useClass: JwtAuthGuard,
|
|
523
|
-
},
|
|
524
|
-
],
|
|
525
|
-
})
|
|
526
|
-
export class AppModule {}
|
|
527
537
|
```
|
|
528
538
|
|
|
529
|
-
**Features:**
|
|
530
|
-
- Validates JWT tokens from `Authorization: Bearer <token>` header
|
|
531
|
-
- Respects `@Public()` decorator to skip authentication
|
|
532
|
-
- Throws `UnauthorizedException` for invalid/expired tokens
|
|
533
|
-
- Works with JwtStrategy registered in auth module
|
|
534
|
-
|
|
535
|
-
**Note:** The JwtStrategy (which registers the JWT validation logic with Passport) is still in the auth module at `@flusys/nestjs-auth/strategies/jwt.strategy.ts`. This guard uses that registered strategy.
|
|
536
|
-
|
|
537
539
|
### PermissionGuard
|
|
538
540
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
The `PermissionGuard` handles permission-based access control.
|
|
541
|
+
Checks user permissions from cache with AND/OR/nested logic support.
|
|
542
542
|
|
|
543
543
|
```typescript
|
|
544
|
-
import { Module } from '@nestjs/common';
|
|
545
|
-
import { APP_GUARD } from '@nestjs/core';
|
|
546
544
|
import { PermissionGuard } from '@flusys/nestjs-shared/guards';
|
|
547
545
|
|
|
546
|
+
// Apply globally
|
|
548
547
|
@Module({
|
|
549
|
-
providers: [
|
|
550
|
-
{
|
|
551
|
-
provide: APP_GUARD,
|
|
552
|
-
useClass: PermissionGuard,
|
|
553
|
-
},
|
|
554
|
-
],
|
|
548
|
+
providers: [{ provide: APP_GUARD, useClass: PermissionGuard }],
|
|
555
549
|
})
|
|
556
550
|
export class AppModule {}
|
|
557
551
|
```
|
|
558
552
|
|
|
559
|
-
|
|
553
|
+
**Cache Key Formats:**
|
|
560
554
|
|
|
561
555
|
```typescript
|
|
562
556
|
// Without company feature
|
|
563
557
|
`permissions:user:{userId}`
|
|
564
558
|
|
|
565
|
-
// With company feature
|
|
559
|
+
// With company feature
|
|
566
560
|
`permissions:company:{companyId}:branch:{branchId}:user:{userId}`
|
|
567
561
|
```
|
|
568
562
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
// Cache TTL in seconds
|
|
575
|
-
cacheTtl: 3600,
|
|
576
|
-
|
|
577
|
-
// Permission check mode
|
|
578
|
-
mode: 'RBAC' | 'DIRECT' | 'FULL',
|
|
579
|
-
|
|
580
|
-
// Custom permission resolver
|
|
581
|
-
resolver: CustomPermissionResolver,
|
|
582
|
-
});
|
|
583
|
-
```
|
|
563
|
+
**Features:**
|
|
564
|
+
- Supports AND/OR operators via `@RequirePermission` and `@RequireAnyPermission`
|
|
565
|
+
- Nested conditions via `@RequirePermissionCondition`
|
|
566
|
+
- Wildcard support (`*` and `*.suffix`)
|
|
567
|
+
- Company/branch scoped permissions
|
|
584
568
|
|
|
585
569
|
---
|
|
586
570
|
|
|
587
571
|
## Middleware
|
|
588
572
|
|
|
589
|
-
### LoggerMiddleware
|
|
573
|
+
### LoggerMiddleware
|
|
590
574
|
|
|
591
575
|
**Location:** `@flusys/nestjs-shared/middlewares/logger.middleware.ts`
|
|
592
576
|
|
|
@@ -877,7 +861,7 @@ export class PostController {
|
|
|
877
861
|
|
|
878
862
|
### Slug Interceptor
|
|
879
863
|
|
|
880
|
-
Auto-generate slugs from name field:
|
|
864
|
+
Auto-generate slugs from name field using UtilsService (injected via DI):
|
|
881
865
|
|
|
882
866
|
```typescript
|
|
883
867
|
import { Slug } from '@flusys/nestjs-shared/interceptors';
|
|
@@ -893,6 +877,8 @@ export class ProductController {
|
|
|
893
877
|
}
|
|
894
878
|
```
|
|
895
879
|
|
|
880
|
+
**Note:** Requires `UtilsModule` to be imported in your module for dependency injection.
|
|
881
|
+
|
|
896
882
|
---
|
|
897
883
|
|
|
898
884
|
## Caching System
|
|
@@ -976,7 +962,7 @@ export class AppModule {}
|
|
|
976
962
|
|
|
977
963
|
## Multi-Tenant DataSource
|
|
978
964
|
|
|
979
|
-
Dynamic database connection management
|
|
965
|
+
Dynamic database connection management with connection pooling and request-scoped tenant resolution.
|
|
980
966
|
|
|
981
967
|
### Setup
|
|
982
968
|
|
|
@@ -986,7 +972,7 @@ import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
|
986
972
|
@Module({
|
|
987
973
|
imports: [
|
|
988
974
|
DataSourceModule.forRoot({
|
|
989
|
-
|
|
975
|
+
bootstrapAppConfig: { databaseMode: 'multi-tenant' },
|
|
990
976
|
defaultDatabaseConfig: {
|
|
991
977
|
type: 'mysql',
|
|
992
978
|
host: 'localhost',
|
|
@@ -994,11 +980,9 @@ import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
|
994
980
|
username: 'root',
|
|
995
981
|
password: 'password',
|
|
996
982
|
},
|
|
997
|
-
|
|
998
|
-
// Tenant configurations
|
|
999
983
|
tenants: [
|
|
1000
|
-
{ id: 'tenant1', database: 'tenant1_db'
|
|
1001
|
-
{ id: 'tenant2', database: 'tenant2_db'
|
|
984
|
+
{ id: 'tenant1', database: 'tenant1_db' },
|
|
985
|
+
{ id: 'tenant2', database: 'tenant2_db' },
|
|
1002
986
|
],
|
|
1003
987
|
}),
|
|
1004
988
|
],
|
|
@@ -1006,41 +990,35 @@ import { DataSourceModule } from '@flusys/nestjs-shared/modules';
|
|
|
1006
990
|
export class AppModule {}
|
|
1007
991
|
```
|
|
1008
992
|
|
|
1009
|
-
###
|
|
993
|
+
### Key Methods
|
|
1010
994
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
995
|
+
| Method | Description |
|
|
996
|
+
|--------|-------------|
|
|
997
|
+
| `getDataSource()` | Get DataSource for current tenant (from header) |
|
|
998
|
+
| `getDataSourceForTenant(id)` | Get DataSource for specific tenant |
|
|
999
|
+
| `getRepository(entity)` | Get repository for current tenant |
|
|
1000
|
+
| `withTenant(id, callback)` | Execute callback with specific tenant DataSource |
|
|
1001
|
+
| `forAllTenants(callback)` | Execute callback for all active tenants |
|
|
1002
|
+
| `registerTenant(config)` | Register new tenant at runtime |
|
|
1003
|
+
| `removeTenant(id)` | Remove tenant and close connection |
|
|
1004
|
+
|
|
1005
|
+
### Usage
|
|
1014
1006
|
|
|
1007
|
+
```typescript
|
|
1015
1008
|
@Injectable()
|
|
1016
1009
|
export class TenantService {
|
|
1017
|
-
constructor(
|
|
1018
|
-
@Inject('DATASOURCE_PROVIDER')
|
|
1019
|
-
private dataSourceProvider: MultiTenantDataSourceService,
|
|
1020
|
-
) {}
|
|
1010
|
+
constructor(private dataSource: MultiTenantDataSourceService) {}
|
|
1021
1011
|
|
|
1022
|
-
async
|
|
1023
|
-
|
|
1012
|
+
async getUsers() {
|
|
1013
|
+
// Auto-resolves tenant from X-Tenant-ID header
|
|
1014
|
+
const repo = await this.dataSource.getRepository(User);
|
|
1015
|
+
return repo.find();
|
|
1024
1016
|
}
|
|
1025
|
-
}
|
|
1026
|
-
```
|
|
1027
1017
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
//
|
|
1032
|
-
@Controller('users')
|
|
1033
|
-
export class UserController {
|
|
1034
|
-
// Automatically uses tenant-specific database
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Manual tenant specification
|
|
1038
|
-
@Injectable()
|
|
1039
|
-
export class CrossTenantService {
|
|
1040
|
-
async copyData(fromTenant: string, toTenant: string) {
|
|
1041
|
-
const fromConn = await this.dataSourceProvider.getConnection(fromTenant);
|
|
1042
|
-
const toConn = await this.dataSourceProvider.getConnection(toTenant);
|
|
1043
|
-
// ...
|
|
1018
|
+
async crossTenantCopy(fromId: string, toId: string) {
|
|
1019
|
+
const fromDs = await this.dataSource.getDataSourceForTenant(fromId);
|
|
1020
|
+
const toDs = await this.dataSource.getDataSourceForTenant(toId);
|
|
1021
|
+
// Copy data between tenants
|
|
1044
1022
|
}
|
|
1045
1023
|
}
|
|
1046
1024
|
```
|
|
@@ -1063,18 +1041,15 @@ import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
|
|
|
1063
1041
|
"category": "electronics"
|
|
1064
1042
|
},
|
|
1065
1043
|
"pagination": {
|
|
1066
|
-
"
|
|
1067
|
-
"
|
|
1044
|
+
"currentPage": 0,
|
|
1045
|
+
"pageSize": 10
|
|
1068
1046
|
},
|
|
1069
1047
|
"sort": {
|
|
1070
|
-
"
|
|
1071
|
-
"
|
|
1072
|
-
},
|
|
1073
|
-
"search": {
|
|
1074
|
-
"fields": ["name", "description"],
|
|
1075
|
-
"value": "laptop"
|
|
1048
|
+
"createdAt": "DESC",
|
|
1049
|
+
"name": "ASC"
|
|
1076
1050
|
},
|
|
1077
|
-
"select": ["id", "name", "price"]
|
|
1051
|
+
"select": ["id", "name", "price"],
|
|
1052
|
+
"withDeleted": false
|
|
1078
1053
|
}
|
|
1079
1054
|
```
|
|
1080
1055
|
|
|
@@ -1235,11 +1210,12 @@ import {
|
|
|
1235
1210
|
Public,
|
|
1236
1211
|
RequirePermission,
|
|
1237
1212
|
RequireAnyPermission,
|
|
1238
|
-
|
|
1213
|
+
RequirePermissionCondition,
|
|
1239
1214
|
} from '@flusys/nestjs-shared/decorators';
|
|
1240
1215
|
|
|
1241
1216
|
// Guards
|
|
1242
1217
|
import {
|
|
1218
|
+
JwtAuthGuard,
|
|
1243
1219
|
PermissionGuard,
|
|
1244
1220
|
} from '@flusys/nestjs-shared/guards';
|
|
1245
1221
|
|
|
@@ -1375,25 +1351,17 @@ async getPermissions(userId: string) {
|
|
|
1375
1351
|
|
|
1376
1352
|
The `@flusys/nestjs-shared` package provides:
|
|
1377
1353
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1354
|
+
- **Generic CRUD** - Consistent API patterns
|
|
1355
|
+
- **Permission System** - Flexible access control
|
|
1356
|
+
- **Caching** - High-performance data access
|
|
1357
|
+
- **Multi-Tenancy** - Database isolation
|
|
1358
|
+
- **Request Correlation** - AsyncLocalStorage-based tracking
|
|
1359
|
+
- **Middleware** - Logging and performance monitoring
|
|
1360
|
+
- **Interceptors** - Request/response processing
|
|
1361
|
+
- **Error Handling** - Centralized error management
|
|
1386
1362
|
|
|
1387
1363
|
This is the shared infrastructure layer used by all other Flusys packages.
|
|
1388
1364
|
|
|
1389
1365
|
---
|
|
1390
1366
|
|
|
1391
|
-
**Last Updated:** 2026-02-
|
|
1392
|
-
**Recent Improvements:**
|
|
1393
|
-
- 2026-02-07: Documentation cleanup - removed outdated JWT configuration section, updated package architecture to match actual structure
|
|
1394
|
-
- 2026-01-14: Enhanced `LoggerMiddleware` with complete request/response details (URL, path, query, headers, user context)
|
|
1395
|
-
- 2026-01-14: Added multiple response hooks (send, json, end) for reliable response capture
|
|
1396
|
-
- 2026-01-14: Improved error logging with stack traces
|
|
1397
|
-
- 2026-01-12: Added `LoggerMiddleware` for request correlation and structured logging
|
|
1398
|
-
- 2026-01-12: Added AsyncLocalStorage-based request context (requestId, tenantId, userId, companyId)
|
|
1399
|
-
- 2026-01-13: Removed JWT config/constants (moved to auth-specific packages)
|
|
1367
|
+
**Last Updated:** 2026-02-18
|
|
@@ -8,10 +8,10 @@ Object.defineProperty(exports, "createApiController", {
|
|
|
8
8
|
return createApiController;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
const _decorators = require("
|
|
12
|
-
const _dtos = require("
|
|
13
|
-
const _guards = require("
|
|
14
|
-
const _interceptors = require("
|
|
11
|
+
const _decorators = require("../decorators");
|
|
12
|
+
const _dtos = require("../dtos");
|
|
13
|
+
const _guards = require("../guards");
|
|
14
|
+
const _interceptors = require("../interceptors");
|
|
15
15
|
const _common = require("@nestjs/common");
|
|
16
16
|
const _swagger = require("@nestjs/swagger");
|
|
17
17
|
const _classtransformer = require("class-transformer");
|
|
@@ -104,8 +104,10 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
104
104
|
// 2. It's an object with 'level' property but no endpoint keys
|
|
105
105
|
const isGlobalSecurity = typeof securityConfig === 'string' || securityConfig && typeof securityConfig === 'object' && 'level' in securityConfig && !endpointKeys.some((key)=>key in securityConfig);
|
|
106
106
|
// Normalize security config for each endpoint
|
|
107
|
+
// IMPORTANT: When per-endpoint security is specified, default to 'jwt' for unconfigured endpoints
|
|
108
|
+
// to prevent accidentally exposing endpoints without authentication
|
|
107
109
|
const defaultSecurity = isGlobalSecurity ? normalizeSecurity(securityConfig) : {
|
|
108
|
-
level: '
|
|
110
|
+
level: 'jwt'
|
|
109
111
|
};
|
|
110
112
|
const security = {
|
|
111
113
|
insert: isGlobalSecurity ? defaultSecurity : normalizeSecurity(securityConfig?.insert),
|
|
@@ -117,9 +119,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
117
119
|
delete: isGlobalSecurity ? defaultSecurity : normalizeSecurity(securityConfig?.delete)
|
|
118
120
|
};
|
|
119
121
|
let ApiController = class ApiController {
|
|
120
|
-
// =========================================================================
|
|
121
|
-
// INSERT (Single Item) - With Idempotency Support
|
|
122
|
-
// =========================================================================
|
|
123
122
|
async insert(addDto, user) {
|
|
124
123
|
const entity = await this.service.insert(addDto, user);
|
|
125
124
|
const data = (0, _classtransformer.plainToInstance)(responseDtoClass, entity);
|
|
@@ -129,9 +128,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
129
128
|
data
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
|
-
// =========================================================================
|
|
133
|
-
// INSERT MANY (Bulk) - With Idempotency Support
|
|
134
|
-
// =========================================================================
|
|
135
131
|
async insertMany(addDto, user) {
|
|
136
132
|
const entities = await this.service.insertMany(addDto, user);
|
|
137
133
|
const data = entities.map((item)=>(0, _classtransformer.plainToInstance)(responseDtoClass, item));
|
|
@@ -146,9 +142,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
146
142
|
}
|
|
147
143
|
};
|
|
148
144
|
}
|
|
149
|
-
// =========================================================================
|
|
150
|
-
// GET BY ID (Single Item)
|
|
151
|
-
// =========================================================================
|
|
152
145
|
async getById(id, body, user) {
|
|
153
146
|
const entity = await this.service.findById(id, user, body?.select);
|
|
154
147
|
const data = (0, _classtransformer.plainToInstance)(responseDtoClass, entity);
|
|
@@ -158,9 +151,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
158
151
|
data
|
|
159
152
|
};
|
|
160
153
|
}
|
|
161
|
-
// =========================================================================
|
|
162
|
-
// UPDATE (Single Item)
|
|
163
|
-
// =========================================================================
|
|
164
154
|
async update(updateDto, user) {
|
|
165
155
|
const entity = await this.service.update(updateDto, user);
|
|
166
156
|
const data = (0, _classtransformer.plainToInstance)(responseDtoClass, entity);
|
|
@@ -170,9 +160,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
170
160
|
data
|
|
171
161
|
};
|
|
172
162
|
}
|
|
173
|
-
// =========================================================================
|
|
174
|
-
// UPDATE MANY (Bulk)
|
|
175
|
-
// =========================================================================
|
|
176
163
|
async updateMany(updateDtos, user) {
|
|
177
164
|
const entities = await this.service.updateMany(updateDtos, user);
|
|
178
165
|
const data = (0, _classtransformer.plainToInstance)(responseDtoClass, entities);
|
|
@@ -187,9 +174,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
187
174
|
}
|
|
188
175
|
};
|
|
189
176
|
}
|
|
190
|
-
// =========================================================================
|
|
191
|
-
// GET ALL (Paginated List)
|
|
192
|
-
// =========================================================================
|
|
193
177
|
async getAll(filterAndPaginationDto, user, search) {
|
|
194
178
|
const result = await this.service.getAll(search ?? '', filterAndPaginationDto, user);
|
|
195
179
|
const data = (0, _classtransformer.plainToInstance)(responseDtoClass, result.data);
|
|
@@ -210,9 +194,6 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
210
194
|
}
|
|
211
195
|
};
|
|
212
196
|
}
|
|
213
|
-
// =========================================================================
|
|
214
|
-
// DELETE (Soft/Restore/Permanent)
|
|
215
|
-
// =========================================================================
|
|
216
197
|
async delete(deleteDto, user) {
|
|
217
198
|
await this.service.delete(deleteDto, user);
|
|
218
199
|
const count = Array.isArray(deleteDto.id) ? deleteDto.id.length : 1;
|
|
@@ -368,16 +349,7 @@ function createApiController(createDtoClass, updateDtoClass, responseDtoClass, o
|
|
|
368
349
|
(0, _common.HttpCode)(_common.HttpStatus.OK),
|
|
369
350
|
(0, _swagger.ApiOperation)({
|
|
370
351
|
summary: 'Get all items with filters and pagination',
|
|
371
|
-
description:
|
|
372
|
-
Retrieves items with support for:
|
|
373
|
-
- **filter**: Apply field-based filters (e.g., \`{ "isActive": true }\`)
|
|
374
|
-
- **pagination**: Control page and page size
|
|
375
|
-
- **sort**: Order by any field (e.g., \`{ "createdAt": "DESC" }\`)
|
|
376
|
-
- **select**: Choose specific fields to return
|
|
377
|
-
- **withDeleted**: Include soft-deleted items
|
|
378
|
-
- **extraKey**: Include additional relations
|
|
379
|
-
- **q** (query param): Global text search
|
|
380
|
-
`
|
|
352
|
+
description: 'Supports filter, pagination, sort, select, withDeleted, and q (search) params'
|
|
381
353
|
}),
|
|
382
354
|
(0, _swagger.ApiQuery)({
|
|
383
355
|
name: 'q',
|
|
@@ -406,15 +378,7 @@ Retrieves items with support for:
|
|
|
406
378
|
(0, _common.HttpCode)(_common.HttpStatus.OK),
|
|
407
379
|
(0, _swagger.ApiOperation)({
|
|
408
380
|
summary: 'Delete, restore, or permanently remove items',
|
|
409
|
-
description:
|
|
410
|
-
Performs one of three actions:
|
|
411
|
-
|
|
412
|
-
- **"delete"** (soft delete): Marks items as deleted but keeps in database
|
|
413
|
-
- **"restore"**: Reverts soft-deleted items to active
|
|
414
|
-
- **"permanent"**: Completely removes items from database
|
|
415
|
-
|
|
416
|
-
Supports single ID or array of IDs for batch operations.
|
|
417
|
-
`
|
|
381
|
+
description: 'Types: delete (soft), restore, permanent. Supports batch IDs.'
|
|
418
382
|
}),
|
|
419
383
|
(0, _swagger.ApiResponse)({
|
|
420
384
|
status: 200,
|
|
@@ -25,9 +25,6 @@ function _define_property(obj, key, value) {
|
|
|
25
25
|
return obj;
|
|
26
26
|
}
|
|
27
27
|
let ApiService = class ApiService {
|
|
28
|
-
// ---------------------------------------------------------------------
|
|
29
|
-
// INSERT SINGLE ENTITY
|
|
30
|
-
// ---------------------------------------------------------------------
|
|
31
28
|
async insert(dto, user) {
|
|
32
29
|
await this.ensureRepositoryInitialized();
|
|
33
30
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
@@ -54,9 +51,6 @@ let ApiService = class ApiService {
|
|
|
54
51
|
await qr.release();
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
|
-
// ---------------------------------------------------------------------
|
|
58
|
-
// INSERT MULTIPLE ENTITIES
|
|
59
|
-
// ---------------------------------------------------------------------
|
|
60
54
|
async insertMany(dtos, user) {
|
|
61
55
|
await this.ensureRepositoryInitialized();
|
|
62
56
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
@@ -87,9 +81,6 @@ let ApiService = class ApiService {
|
|
|
87
81
|
await qr.release();
|
|
88
82
|
}
|
|
89
83
|
}
|
|
90
|
-
// ---------------------------------------------------------------------
|
|
91
|
-
// UPDATE SINGLE ENTITY
|
|
92
|
-
// ---------------------------------------------------------------------
|
|
93
84
|
async update(dto, user) {
|
|
94
85
|
await this.ensureRepositoryInitialized();
|
|
95
86
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
@@ -116,9 +107,6 @@ let ApiService = class ApiService {
|
|
|
116
107
|
await qr.release();
|
|
117
108
|
}
|
|
118
109
|
}
|
|
119
|
-
// ---------------------------------------------------------------------
|
|
120
|
-
// UPDATE MULTIPLE ENTITIES
|
|
121
|
-
// ---------------------------------------------------------------------
|
|
122
110
|
async updateMany(dtos, user) {
|
|
123
111
|
await this.ensureRepositoryInitialized();
|
|
124
112
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
@@ -149,9 +137,6 @@ let ApiService = class ApiService {
|
|
|
149
137
|
await qr.release();
|
|
150
138
|
}
|
|
151
139
|
}
|
|
152
|
-
// ---------------------------------------------------------------------
|
|
153
|
-
// FIND BY IDS
|
|
154
|
-
// ---------------------------------------------------------------------
|
|
155
140
|
async findByIds(ids, user) {
|
|
156
141
|
await this.ensureRepositoryInitialized();
|
|
157
142
|
try {
|
|
@@ -176,9 +161,6 @@ let ApiService = class ApiService {
|
|
|
176
161
|
this.handleError(error, 'findByIds');
|
|
177
162
|
}
|
|
178
163
|
}
|
|
179
|
-
// ---------------------------------------------------------------------
|
|
180
|
-
// FIND BY ID
|
|
181
|
-
// ---------------------------------------------------------------------
|
|
182
164
|
async findById(id, user, select) {
|
|
183
165
|
await this.ensureRepositoryInitialized();
|
|
184
166
|
try {
|
|
@@ -217,9 +199,6 @@ let ApiService = class ApiService {
|
|
|
217
199
|
this.handleError(error, 'findById');
|
|
218
200
|
}
|
|
219
201
|
}
|
|
220
|
-
// ---------------------------------------------------------------------
|
|
221
|
-
// GET ALL (WITH FILTER, SORT, SEARCH, PAGINATION & CACHING)
|
|
222
|
-
// ---------------------------------------------------------------------
|
|
223
202
|
async getAll(search, filterAndPaginationDto, user) {
|
|
224
203
|
await this.ensureRepositoryInitialized();
|
|
225
204
|
try {
|
|
@@ -318,9 +297,6 @@ let ApiService = class ApiService {
|
|
|
318
297
|
this.handleError(error, 'getAll');
|
|
319
298
|
}
|
|
320
299
|
}
|
|
321
|
-
// ---------------------------------------------------------------------
|
|
322
|
-
// DELETE / RESTORE / HARD DELETE
|
|
323
|
-
// ---------------------------------------------------------------------
|
|
324
300
|
async delete(option, user) {
|
|
325
301
|
await this.ensureRepositoryInitialized();
|
|
326
302
|
const queryRunner = this.repository.manager.connection.createQueryRunner();
|
|
@@ -363,9 +339,7 @@ let ApiService = class ApiService {
|
|
|
363
339
|
await queryRunner.release();
|
|
364
340
|
}
|
|
365
341
|
}
|
|
366
|
-
//
|
|
367
|
-
// CACHING HELPERS
|
|
368
|
-
// ---------------------------------------------------------------------
|
|
342
|
+
// Caching
|
|
369
343
|
async clearCacheForAll() {
|
|
370
344
|
await this.utilsService.clearCache(this.entityName, this.cacheManager);
|
|
371
345
|
}
|
|
@@ -380,17 +354,8 @@ let ApiService = class ApiService {
|
|
|
380
354
|
});
|
|
381
355
|
_errorhandlerutil.ErrorHandler.rethrowError(error);
|
|
382
356
|
}
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
// ---------------------------------------------------------------------
|
|
386
|
-
/**
|
|
387
|
-
* Hook called before ANY repository access
|
|
388
|
-
* CRITICAL: Override this in REQUEST-scoped services that use lazy repository initialization
|
|
389
|
-
* Example use case: DataSource Provider pattern where repository is set dynamically
|
|
390
|
-
*/ async ensureRepositoryInitialized() {
|
|
391
|
-
// Default: no-op - repository is already initialized in constructor
|
|
392
|
-
// Override in child classes that need lazy initialization
|
|
393
|
-
}
|
|
357
|
+
// Hooks (override in child classes)
|
|
358
|
+
async ensureRepositoryInitialized() {}
|
|
394
359
|
async beforeInsertOperation(_dto, _user, _queryRunner) {}
|
|
395
360
|
async afterInsertOperation(_entity, _user, _queryRunner) {}
|
|
396
361
|
async beforeUpdateOperation(_dto, _user, _queryRunner) {}
|
|
@@ -439,9 +404,7 @@ let ApiService = class ApiService {
|
|
|
439
404
|
isRaw: false
|
|
440
405
|
};
|
|
441
406
|
}
|
|
442
|
-
//
|
|
443
|
-
// DTO <-> ENTITY CONVERSION
|
|
444
|
-
// ---------------------------------------------------------------------
|
|
407
|
+
// DTO conversion
|
|
445
408
|
async convertRequestDtoToEntity(dto, user) {
|
|
446
409
|
return Array.isArray(dto) ? await this.convertArrayDtoToEntities(dto, user) : [
|
|
447
410
|
await this.convertSingleDtoToEntity(dto, user)
|
package/cjs/classes/index.js
CHANGED
|
@@ -8,6 +8,7 @@ _export_star(require("./request-scoped-api.service"), exports);
|
|
|
8
8
|
_export_star(require("./hybrid-cache.class"), exports);
|
|
9
9
|
_export_star(require("./winston-logger-adapter.class"), exports);
|
|
10
10
|
_export_star(require("./winston.logger.class"), exports);
|
|
11
|
+
_export_star(require("../constants/permissions"), exports);
|
|
11
12
|
function _export_star(from, to) {
|
|
12
13
|
Object.keys(from).forEach(function(k) {
|
|
13
14
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|