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 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.