nicot 1.2.12 → 1.2.13
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 +408 -0
- package/dist/index.cjs +744 -3
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +767 -3
- package/dist/index.mjs.map +4 -4
- package/dist/src/transactional-typeorm.module.d.ts +20 -0
- package/dist/src/utility/create-dynamic-fetcher-proxy.d.ts +1 -0
- package/dist/src/utility/create-inject-from-token-factory.d.ts +4 -0
- package/index.ts +1 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1169,6 +1169,414 @@ You can still build custom endpoints and return these wrappers manually if neede
|
|
|
1169
1169
|
|
|
1170
1170
|
---
|
|
1171
1171
|
|
|
1172
|
+
## Transactional TypeORM (request-scoped transactions)
|
|
1173
|
+
|
|
1174
|
+
NICOT’s CRUD flows are TypeORM-based, but by default each repository call is not automatically wrapped in a single database transaction.
|
|
1175
|
+
|
|
1176
|
+
If you want **“one HTTP request = one DB transaction”**, NICOT provides a small TypeORM wrapper:
|
|
1177
|
+
|
|
1178
|
+
- `TransactionalTypeOrmInterceptor()` — starts a TypeORM transaction at the beginning of a request and commits or rolls it back when request processing finishes or fails.
|
|
1179
|
+
- `TransactionalTypeOrmModule.forFeature(...)` — provides **request-scoped** transaction-aware `EntityManager` / `Repository` injection tokens, and also includes the TypeORM `forFeature()` import/export.
|
|
1180
|
+
|
|
1181
|
+
### When to use
|
|
1182
|
+
|
|
1183
|
+
Use transactional mode when you want:
|
|
1184
|
+
|
|
1185
|
+
- multiple writes across different repositories to **commit/rollback together**
|
|
1186
|
+
- service methods that mix `create/update/delete` and custom repo operations
|
|
1187
|
+
- deterministic rollback when you throw `BlankReturnMessageDto(...).toException()`
|
|
1188
|
+
|
|
1189
|
+
Avoid it for:
|
|
1190
|
+
|
|
1191
|
+
- streaming responses (SSE / long-lived streams) — the transaction would stay open until the stream completes
|
|
1192
|
+
- very heavy read-only endpoints where a transaction adds overhead
|
|
1193
|
+
|
|
1194
|
+
### 1) Import TransactionalTypeOrmModule
|
|
1195
|
+
|
|
1196
|
+
In the module that owns your resource:
|
|
1197
|
+
|
|
1198
|
+
```ts
|
|
1199
|
+
import { Module } from '@nestjs/common';
|
|
1200
|
+
import { User } from './user.entity';
|
|
1201
|
+
import { UserController } from './user.controller';
|
|
1202
|
+
import { UserService } from './user.service';
|
|
1203
|
+
|
|
1204
|
+
import { TransactionalTypeOrmModule } from 'nicot'; // or your local path
|
|
1205
|
+
|
|
1206
|
+
@Module({
|
|
1207
|
+
imports: [
|
|
1208
|
+
// ⭐ includes TypeOrmModule.forFeature([User]) internally and re-exports it
|
|
1209
|
+
TransactionalTypeOrmModule.forFeature([User]),
|
|
1210
|
+
],
|
|
1211
|
+
controllers: [UserController],
|
|
1212
|
+
providers: [UserService],
|
|
1213
|
+
})
|
|
1214
|
+
export class UserModule {}
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
Notes:
|
|
1218
|
+
|
|
1219
|
+
- The providers created by `TransactionalTypeOrmModule.forFeature(...)` are `Scope.REQUEST`.
|
|
1220
|
+
- You still need a `TypeOrmModule.forRoot(...)` (or equivalent) at app root to configure the DataSource.
|
|
1221
|
+
|
|
1222
|
+
### 2) Enable TransactionalTypeOrmInterceptor() for the controller
|
|
1223
|
+
|
|
1224
|
+
Apply the interceptor to ensure a transaction is created for each HTTP request:
|
|
1225
|
+
|
|
1226
|
+
```
|
|
1227
|
+
import { Controller, UseInterceptors } from '@nestjs/common';
|
|
1228
|
+
import { RestfulFactory } from 'nicot';
|
|
1229
|
+
import { User } from './user.entity';
|
|
1230
|
+
import { TransactionalTypeOrmInterceptor } from 'nicot';
|
|
1231
|
+
|
|
1232
|
+
export const UserFactory = new RestfulFactory(User);
|
|
1233
|
+
|
|
1234
|
+
@Controller('users')
|
|
1235
|
+
@UseInterceptors(TransactionalTypeOrmInterceptor())
|
|
1236
|
+
export class UserController extends UserFactory.baseController() {
|
|
1237
|
+
constructor(service: UserService) {
|
|
1238
|
+
super(service);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
Behavior:
|
|
1244
|
+
|
|
1245
|
+
- Transaction begins before controller handler runs.
|
|
1246
|
+
- Transaction commits when the returned Observable completes.
|
|
1247
|
+
- Transaction rolls back when the Observable errors (including thrown HTTP exceptions).
|
|
1248
|
+
|
|
1249
|
+
### 3) Inject Transactional Repository / EntityManager in services
|
|
1250
|
+
|
|
1251
|
+
To actually use the transaction context, inject the transactional repo/em instead of the default TypeORM ones.
|
|
1252
|
+
|
|
1253
|
+
#### Transactional repository (recommended)
|
|
1254
|
+
|
|
1255
|
+
```ts
|
|
1256
|
+
import { Injectable } from '@nestjs/common';
|
|
1257
|
+
import { Repository } from 'typeorm';
|
|
1258
|
+
import { RestfulFactory } from 'nicot';
|
|
1259
|
+
import { InjectTransactionalRepository } from 'nicot';
|
|
1260
|
+
import { User } from './user.entity';
|
|
1261
|
+
|
|
1262
|
+
export const UserFactory = new RestfulFactory(User);
|
|
1263
|
+
|
|
1264
|
+
@Injectable()
|
|
1265
|
+
export class UserService extends UserFactory.crudService() {
|
|
1266
|
+
constructor(
|
|
1267
|
+
@InjectTransactionalRepository(User)
|
|
1268
|
+
repo: Repository<User>,
|
|
1269
|
+
) {
|
|
1270
|
+
super(repo);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
Now all NICOT CRUD operations (`create/findAll/update/delete/import`) will run using the transaction-bound repository when the interceptor is active.
|
|
1276
|
+
|
|
1277
|
+
#### Transactional entity manager (advanced)
|
|
1278
|
+
|
|
1279
|
+
```ts
|
|
1280
|
+
import { Injectable } from '@nestjs/common';
|
|
1281
|
+
import { EntityManager } from 'typeorm';
|
|
1282
|
+
import { InjectTransactionalEntityManager } from 'nicot';
|
|
1283
|
+
|
|
1284
|
+
@Injectable()
|
|
1285
|
+
export class UserTxService {
|
|
1286
|
+
constructor(
|
|
1287
|
+
@InjectTransactionalEntityManager()
|
|
1288
|
+
private readonly em: EntityManager,
|
|
1289
|
+
) {}
|
|
1290
|
+
|
|
1291
|
+
async doSomethingComplex() {
|
|
1292
|
+
// this.em is transaction-bound when interceptor is active
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
### Rollback example (404)
|
|
1298
|
+
|
|
1299
|
+
If you throw a NICOT exception after writing, the transaction will roll back:
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
@Post('fail')
|
|
1303
|
+
async fail() {
|
|
1304
|
+
await this.service.repo.save({ name: 'ROLL' } as any);
|
|
1305
|
+
throw new BlankReturnMessageDto(404, 'message').toException();
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
Expected:
|
|
1310
|
+
|
|
1311
|
+
- HTTP response is `404`
|
|
1312
|
+
- database changes are not committed (rollback)
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
## Operation: Atomic business logic on a single entity
|
|
1317
|
+
|
|
1318
|
+
NICOT provides an **`operation` abstraction** for implementing
|
|
1319
|
+
**atomic, row-level business logic** on top of a `CrudService`.
|
|
1320
|
+
|
|
1321
|
+
This abstraction exists on **two different layers**:
|
|
1322
|
+
|
|
1323
|
+
1. **Service layer**: `CrudService.operation()`
|
|
1324
|
+
2. **Controller layer**: `RestfulFactory.operation()`
|
|
1325
|
+
|
|
1326
|
+
They are designed to be **orthogonal**:
|
|
1327
|
+
- the service operation **executes domain logic**
|
|
1328
|
+
- the factory operation **exposes it as an HTTP endpoint**
|
|
1329
|
+
|
|
1330
|
+
You may use either one independently, or combine them.
|
|
1331
|
+
|
|
1332
|
+
---
|
|
1333
|
+
|
|
1334
|
+
### Service-level operation (`CrudService.operation()`)
|
|
1335
|
+
|
|
1336
|
+
#### What it does
|
|
1337
|
+
|
|
1338
|
+
`CrudService.operation()` executes a callback with:
|
|
1339
|
+
|
|
1340
|
+
- a **row-level write lock** on the target entity
|
|
1341
|
+
- a transactional repository (unless one is explicitly provided)
|
|
1342
|
+
- automatic **change tracking and flushing**
|
|
1343
|
+
- full compatibility with NICOT binding (`@BindingColumn`, `@BindingValue`)
|
|
1344
|
+
|
|
1345
|
+
Internally, it follows this lifecycle:
|
|
1346
|
+
|
|
1347
|
+
1. Resolve binding values (tenant / owner isolation)
|
|
1348
|
+
2. Check entity existence
|
|
1349
|
+
3. Open a transaction (unless `options.repo` is provided)
|
|
1350
|
+
4. Load the entity with `pessimistic_write` lock
|
|
1351
|
+
5. Snapshot column values
|
|
1352
|
+
6. Run user callback
|
|
1353
|
+
7. Flush only changed columns
|
|
1354
|
+
8. Commit or rollback
|
|
1355
|
+
|
|
1356
|
+
---
|
|
1357
|
+
|
|
1358
|
+
#### Basic usage (inside a service)
|
|
1359
|
+
|
|
1360
|
+
```ts
|
|
1361
|
+
@Injectable()
|
|
1362
|
+
class UserService extends UserFactory.crudService() {
|
|
1363
|
+
async disableUser(id: number) {
|
|
1364
|
+
return this.operation(id, async (user) => {
|
|
1365
|
+
user.isActive = false;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
Return behavior:
|
|
1372
|
+
|
|
1373
|
+
- callback returns `void | undefined | null`
|
|
1374
|
+
→ `BlankReturnMessageDto(200, 'success')`
|
|
1375
|
+
- callback returns a value
|
|
1376
|
+
→ `GenericReturnMessageDto(200, 'success', value)`
|
|
1377
|
+
|
|
1378
|
+
---
|
|
1379
|
+
|
|
1380
|
+
#### Returning business data
|
|
1381
|
+
|
|
1382
|
+
```ts
|
|
1383
|
+
async disableAndReport(id: number) {
|
|
1384
|
+
return this.operation(id, async (user) => {
|
|
1385
|
+
user.isActive = false;
|
|
1386
|
+
return { disabled: true };
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
---
|
|
1392
|
+
|
|
1393
|
+
#### Error handling & rollback
|
|
1394
|
+
|
|
1395
|
+
Any exception thrown inside the callback causes a rollback.
|
|
1396
|
+
|
|
1397
|
+
```ts
|
|
1398
|
+
async dangerousOperation(id: number) {
|
|
1399
|
+
return this.operation(id, async () => {
|
|
1400
|
+
throw new BlankReturnMessageDto(403, 'Forbidden').toException();
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
---
|
|
1406
|
+
|
|
1407
|
+
#### Binding-aware by default
|
|
1408
|
+
|
|
1409
|
+
`operation()` never bypasses NICOT binding rules.
|
|
1410
|
+
|
|
1411
|
+
If your entity has:
|
|
1412
|
+
|
|
1413
|
+
```ts
|
|
1414
|
+
@BindingColumn()
|
|
1415
|
+
userId: number;
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
and your service defines:
|
|
1419
|
+
|
|
1420
|
+
```ts
|
|
1421
|
+
@BindingValue()
|
|
1422
|
+
get currentUserId() {
|
|
1423
|
+
return this.ctx.userId;
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
then `operation()` will automatically:
|
|
1428
|
+
|
|
1429
|
+
- restrict existence checks
|
|
1430
|
+
- restrict row locking
|
|
1431
|
+
- restrict updates
|
|
1432
|
+
|
|
1433
|
+
to the current binding scope.
|
|
1434
|
+
|
|
1435
|
+
---
|
|
1436
|
+
|
|
1437
|
+
#### Using `options.repo` (integration with transactional interceptors)
|
|
1438
|
+
|
|
1439
|
+
By default, `operation()` opens its own transaction.
|
|
1440
|
+
|
|
1441
|
+
If you pass a repository via `options.repo`,
|
|
1442
|
+
**no new transaction will be created**.
|
|
1443
|
+
|
|
1444
|
+
This is intended for integration with
|
|
1445
|
+
`TransactionalTypeOrmModule` and `TransactionalTypeOrmInterceptor`.
|
|
1446
|
+
|
|
1447
|
+
```ts
|
|
1448
|
+
@Injectable()
|
|
1449
|
+
class UserService extends UserFactory.crudService() {
|
|
1450
|
+
constructor(
|
|
1451
|
+
@InjectTransactionalRepository(User)
|
|
1452
|
+
private readonly repo: Repository<User>,
|
|
1453
|
+
) {
|
|
1454
|
+
super(repo);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async updateInsideRequestTransaction(id: number) {
|
|
1458
|
+
return this.operation(
|
|
1459
|
+
id,
|
|
1460
|
+
async (user) => {
|
|
1461
|
+
user.name = 'Updated in request transaction';
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
repo: this.repo,
|
|
1465
|
+
},
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
This allows:
|
|
1472
|
+
|
|
1473
|
+
- request-wide transactions
|
|
1474
|
+
- consistent behavior across multiple service calls
|
|
1475
|
+
- zero coupling between business logic and infrastructure
|
|
1476
|
+
|
|
1477
|
+
---
|
|
1478
|
+
|
|
1479
|
+
### Controller-level operation (`RestfulFactory.operation()`)
|
|
1480
|
+
|
|
1481
|
+
#### What it does (and what it does NOT)
|
|
1482
|
+
|
|
1483
|
+
`RestfulFactory.operation()` **does not implement any business logic**.
|
|
1484
|
+
|
|
1485
|
+
It only:
|
|
1486
|
+
|
|
1487
|
+
- declares an HTTP endpoint
|
|
1488
|
+
- wires Swagger metadata
|
|
1489
|
+
- standardizes request / response shape
|
|
1490
|
+
- delegates execution to your service method
|
|
1491
|
+
|
|
1492
|
+
Think of it as **a declarative endpoint generator**, not an executor.
|
|
1493
|
+
|
|
1494
|
+
---
|
|
1495
|
+
|
|
1496
|
+
#### Declaring an operation endpoint
|
|
1497
|
+
|
|
1498
|
+
```ts
|
|
1499
|
+
@Controller('users')
|
|
1500
|
+
export class UserController {
|
|
1501
|
+
constructor(private readonly userService: UserService) {}
|
|
1502
|
+
|
|
1503
|
+
@UserFactory.operation('disable')
|
|
1504
|
+
async disable(@UserFactory.idParam() id: number) {
|
|
1505
|
+
return this.userService.disableUser(id);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
This generates:
|
|
1511
|
+
|
|
1512
|
+
- `POST /users/:id/disable`
|
|
1513
|
+
- Swagger operation summary
|
|
1514
|
+
- standardized NICOT response envelope
|
|
1515
|
+
|
|
1516
|
+
---
|
|
1517
|
+
|
|
1518
|
+
#### Returning custom data
|
|
1519
|
+
|
|
1520
|
+
```ts
|
|
1521
|
+
@UserFactory.operation('reset-password', {
|
|
1522
|
+
returnType: ResetPasswordResultDto,
|
|
1523
|
+
})
|
|
1524
|
+
async resetPassword(@UserFactory.idParam() id: number) {
|
|
1525
|
+
return this.userService.resetPassword(id);
|
|
1526
|
+
}
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
---
|
|
1530
|
+
|
|
1531
|
+
### Combining both layers (recommended pattern)
|
|
1532
|
+
|
|
1533
|
+
The **recommended pattern** is:
|
|
1534
|
+
|
|
1535
|
+
- put **all business logic** in `CrudService.operation()`
|
|
1536
|
+
- expose it via `RestfulFactory.operation()`
|
|
1537
|
+
|
|
1538
|
+
This gives you:
|
|
1539
|
+
|
|
1540
|
+
- reusable domain logic
|
|
1541
|
+
- testable service methods
|
|
1542
|
+
- thin, declarative controllers
|
|
1543
|
+
|
|
1544
|
+
```ts
|
|
1545
|
+
@Injectable()
|
|
1546
|
+
class UserService extends UserFactory.crudService() {
|
|
1547
|
+
async disableUser(id: number) {
|
|
1548
|
+
return this.operation(id, async (user) => {
|
|
1549
|
+
user.isActive = false;
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
@Controller('users')
|
|
1555
|
+
class UserController {
|
|
1556
|
+
constructor(private readonly userService: UserService) {}
|
|
1557
|
+
|
|
1558
|
+
@UserFactory.operation('disable')
|
|
1559
|
+
disable(@UserFactory.idParam() id: number) {
|
|
1560
|
+
return this.userService.disableUser(id);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
---
|
|
1566
|
+
|
|
1567
|
+
### Design philosophy
|
|
1568
|
+
|
|
1569
|
+
- `operation()` is **not** a CRUD replacement
|
|
1570
|
+
- it is **not** a generic transaction wrapper
|
|
1571
|
+
- it is a **domain-oriented mutation primitive**
|
|
1572
|
+
|
|
1573
|
+
In NICOT:
|
|
1574
|
+
|
|
1575
|
+
> **CRUD is declarative**
|
|
1576
|
+
> **Operations express intent**
|
|
1577
|
+
|
|
1578
|
+
---
|
|
1579
|
+
|
|
1172
1580
|
## Best practices
|
|
1173
1581
|
|
|
1174
1582
|
- **One factory per entity**, in its own `*.factory.ts` file.
|