@jsfsi-core/ts-nodejs 1.1.2 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +820 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,820 @@
1
+ # @jsfsi-core/ts-nodejs
2
+
3
+ Node.js-specific utilities for database management, logging, and environment configuration following hexagonal architecture principles.
4
+
5
+ ## 📦 Installation
6
+
7
+ ```bash
8
+ npm install @jsfsi-core/ts-nodejs
9
+ ```
10
+
11
+ **Dependencies:**
12
+
13
+ - `typeorm` - TypeORM for database management
14
+ - `dotenv` - Environment variable loading
15
+
16
+ ## 🏗️ Architecture
17
+
18
+ This package provides Node.js-specific implementations for:
19
+
20
+ - **Database**: Transactional repositories with TypeORM integration
21
+ - **Logging**: Structured logging interface with multiple implementations
22
+ - **Environment**: Type-safe environment variable loading
23
+
24
+ ### Package Structure
25
+
26
+ ```
27
+ src/
28
+ ├── database/
29
+ │ ├── TransactionalRepository.ts # Base transactional repository
30
+ │ ├── TransactionalEntity.ts # Entity interface
31
+ │ └── postgres/ # PostgreSQL utilities
32
+ ├── logger/
33
+ │ ├── Logger.ts # Logger interface
34
+ │ ├── GCPLogger.ts # Google Cloud Platform logger
35
+ │ └── MockLogger.ts # Test logger
36
+ └── env/
37
+ └── env.loader.ts # Environment loader
38
+ ```
39
+
40
+ ## 📋 Features
41
+
42
+ ### Transactional Repository
43
+
44
+ Type-safe transactional repository base class for database operations:
45
+
46
+ ```typescript
47
+ import { TransactionalRepository } from '@jsfsi-core/ts-nodejs';
48
+ import { DataSource } from 'typeorm';
49
+ import { UserEntity } from './entities/UserEntity';
50
+
51
+ export class UserRepository extends TransactionalRepository {
52
+ constructor(dataSource: DataSource) {
53
+ super(dataSource);
54
+ }
55
+
56
+ async findById(id: string): Promise<UserEntity | null> {
57
+ const repository = this.getRepository(UserEntity);
58
+ return repository.findOne({ where: { id } });
59
+ }
60
+
61
+ async save(user: UserEntity): Promise<UserEntity> {
62
+ const repository = this.getRepository(UserEntity);
63
+ return repository.save(user);
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Transactions
69
+
70
+ Execute operations within a transaction:
71
+
72
+ ```typescript
73
+ async function createUserWithProfile(
74
+ userData: CreateUserData,
75
+ profileData: CreateProfileData,
76
+ ): Promise<User> {
77
+ return this.userRepository.withTransaction(async (userRepo) => {
78
+ // All operations within this callback run in a single transaction
79
+ const user = await userRepo.save(createUserEntity(userData));
80
+
81
+ const profileRepo = this.profileRepository.withRepositoryManager(userRepo);
82
+ const profile = await profileRepo.save(createProfileEntity(user.id, profileData));
83
+
84
+ return { user, profile };
85
+ });
86
+ }
87
+ ```
88
+
89
+ ### Transactions as Domain Concepts
90
+
91
+ **Transactions are domain concepts, not persistence concepts.**
92
+
93
+ A transaction represents a **business operation** that must be atomic - it either completes entirely or fails entirely. The transactional repository allows you to move this concept to the domain layer, abstracting the persistence implementation.
94
+
95
+ #### Why Transactions Belong to Domain
96
+
97
+ Transactions express business rules about consistency and atomicity:
98
+
99
+ - **Business Rules**: "When creating an order, both the order and payment must succeed together"
100
+ - **Consistency**: "User registration includes creating a profile and sending a welcome email - all must succeed or all must fail"
101
+ - **Atomicity**: "Inventory deduction and order creation must happen together"
102
+
103
+ The transactional repository abstraction allows domain services to express these business rules without being tied to a specific persistence technology (TypeORM, Prisma, etc.).
104
+
105
+ #### Transactions with External Services
106
+
107
+ Transactions can include **any operations** that should be part of an atomic business operation, including external API calls. If an external service fails, the transaction should rollback:
108
+
109
+ ```typescript
110
+ // Domain service expressing a business operation
111
+ export class OrderService {
112
+ constructor(
113
+ private readonly orderRepository: OrderRepository,
114
+ private readonly inventoryRepository: InventoryRepository,
115
+ private readonly paymentService: PaymentService, // External service adapter
116
+ ) {}
117
+
118
+ async createOrder(orderData: CreateOrderData): Promise<Result<Order, CreateOrderFailure>> {
119
+ // This is a domain concept: "Create order" is a single atomic business operation
120
+ return this.orderRepository.withTransaction(async (orderRepo) => {
121
+ // Step 1: Create order in database
122
+ const [order, orderFailure] = await orderRepo.save(createOrderEntity(orderData));
123
+ if (isFailure(SaveOrderFailure)(orderFailure)) {
124
+ return Fail(orderFailure);
125
+ }
126
+
127
+ // Step 2: Deduct inventory in database
128
+ const inventoryRepo = this.inventoryRepository.withRepositoryManager(orderRepo);
129
+ const [inventory, inventoryFailure] = await inventoryRepo.deductStock(orderData.items);
130
+ if (isFailure(DeductInventoryFailure)(inventoryFailure)) {
131
+ // Transaction automatically rolls back order creation
132
+ return Fail(inventoryFailure);
133
+ }
134
+
135
+ // Step 3: Charge payment via external API
136
+ // This is part of the same business transaction!
137
+ const [payment, paymentFailure] = await this.paymentService.chargePayment({
138
+ orderId: order.id,
139
+ amount: order.total,
140
+ customerId: order.customerId,
141
+ });
142
+
143
+ if (isFailure(PaymentFailure)(paymentFailure)) {
144
+ // If payment fails, the transaction rolls back:
145
+ // - Order is NOT created
146
+ // - Inventory is NOT deducted
147
+ // - Payment is NOT charged
148
+ // All operations are atomic
149
+ return Fail(paymentFailure);
150
+ }
151
+
152
+ // All operations succeeded - transaction commits:
153
+ // - Order is created
154
+ // - Inventory is deducted
155
+ // - Payment is charged
156
+ return Ok(order);
157
+ });
158
+ }
159
+ }
160
+ ```
161
+
162
+ #### Example: User Registration with External Service
163
+
164
+ Another example showing how transactions abstract persistence and include external operations:
165
+
166
+ ```typescript
167
+ export class UserService {
168
+ constructor(
169
+ private readonly userRepository: UserRepository,
170
+ private readonly profileRepository: ProfileRepository,
171
+ private readonly emailService: EmailService, // External service
172
+ private readonly auditService: AuditService, // External service
173
+ ) {}
174
+
175
+ async registerUser(
176
+ registrationData: RegistrationData,
177
+ ): Promise<Result<User, RegistrationFailure>> {
178
+ // Domain concept: "User registration" is an atomic business operation
179
+ return this.userRepository.withTransaction(async (userRepo) => {
180
+ // Step 1: Create user in database
181
+ const [user, userFailure] = await userRepo.save(createUserEntity(registrationData));
182
+ if (isFailure(SaveUserFailure)(userFailure)) {
183
+ return Fail(userFailure);
184
+ }
185
+
186
+ // Step 2: Create profile in database
187
+ const profileRepo = this.profileRepository.withRepositoryManager(userRepo);
188
+ const [profile, profileFailure] = await profileRepo.save(
189
+ createProfileEntity(user.id, registrationData.profile),
190
+ );
191
+ if (isFailure(SaveProfileFailure)(profileFailure)) {
192
+ // Transaction rolls back user creation
193
+ return Fail(profileFailure);
194
+ }
195
+
196
+ // Step 3: Send welcome email via external API
197
+ const [emailSent, emailFailure] = await this.emailService.sendWelcomeEmail(user.email);
198
+ if (isFailure(EmailServiceFailure)(emailFailure)) {
199
+ // If email fails, rollback entire registration:
200
+ // - User is NOT created
201
+ // - Profile is NOT created
202
+ // - Email is NOT sent
203
+ return Fail(emailFailure);
204
+ }
205
+
206
+ // Step 4: Log audit event to external audit service
207
+ const [auditLogged, auditFailure] = await this.auditService.logEvent({
208
+ event: 'USER_REGISTERED',
209
+ userId: user.id,
210
+ timestamp: new Date(),
211
+ });
212
+
213
+ if (isFailure(AuditServiceFailure)(auditFailure)) {
214
+ // If audit logging fails, rollback everything
215
+ return Fail(auditFailure);
216
+ }
217
+
218
+ // All operations succeeded - transaction commits
219
+ return Ok(user);
220
+ });
221
+ }
222
+ }
223
+ ```
224
+
225
+ #### Key Benefits
226
+
227
+ 1. **Domain Abstraction**: Transactions are expressed as domain concepts, not database concepts
228
+ 2. **Persistence Independence**: Can switch database implementations without changing domain logic
229
+ 3. **Atomic Business Operations**: Express business rules about what operations must succeed together
230
+ 4. **External Service Integration**: Include external API calls as part of atomic business operations
231
+ 5. **Consistency**: Ensure all operations in a business transaction succeed or all fail
232
+
233
+ ### Transaction Propagation
234
+
235
+ Share transactions across repositories:
236
+
237
+ ```typescript
238
+ async function updateUserAndOrders(userId: string, updates: UserUpdates): Promise<void> {
239
+ return this.userRepository.withTransaction(async (userRepo) => {
240
+ // Update user
241
+ await userRepo.save(updatedUser);
242
+
243
+ // Use same transaction for order repository
244
+ const orderRepo = this.orderRepository.withRepositoryManager(userRepo);
245
+ await orderRepo.updateOrdersForUser(userId, updates);
246
+ });
247
+ }
248
+ ```
249
+
250
+ ### Locking
251
+
252
+ Use pessimistic locking for concurrent operations:
253
+
254
+ ```typescript
255
+ async function findByIdWithLock(id: string): Promise<UserEntity | null> {
256
+ const repository = this.getRepository(UserEntity);
257
+ return repository.findOne({
258
+ where: { id },
259
+ lock: this.lockInTransaction('pessimistic_write'),
260
+ });
261
+ }
262
+ ```
263
+
264
+ ### Logger
265
+
266
+ Structured logging interface:
267
+
268
+ ```typescript
269
+ import { Logger } from '@jsfsi-core/ts-nodejs';
270
+
271
+ export class MyService {
272
+ constructor(private readonly logger: Logger) {}
273
+
274
+ async processOrder(orderId: string) {
275
+ this.logger.log('Processing order', { orderId });
276
+
277
+ try {
278
+ // Process order
279
+ this.logger.verbose('Order processed successfully', { orderId });
280
+ } catch (error) {
281
+ this.logger.error('Failed to process order', { orderId, error });
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### Log Levels
289
+
290
+ ```typescript
291
+ import { Logger, LogLevel } from '@jsfsi-core/ts-nodejs';
292
+
293
+ // Available log levels
294
+ type LogLevel = 'debug' | 'verbose' | 'log' | 'warn' | 'error' | 'fatal';
295
+
296
+ // Set log levels
297
+ logger.setLogLevels(['log', 'warn', 'error']);
298
+ ```
299
+
300
+ ### Logger Implementations
301
+
302
+ #### Console Logger
303
+
304
+ Basic console logger (for development):
305
+
306
+ ```typescript
307
+ import { ConsoleLogger } from './logger/ConsoleLogger';
308
+
309
+ const logger = new ConsoleLogger();
310
+ logger.log('Hello world');
311
+ ```
312
+
313
+ #### GCP Logger
314
+
315
+ Google Cloud Platform structured logger compatible with **NestJS LoggerService interface**.
316
+
317
+ The GCP Logger automatically performs **data sanitization and redaction** for sensitive keys, ensuring that sensitive information (passwords, tokens, API keys, etc.) is never logged:
318
+
319
+ ```typescript
320
+ import { GCPLogger } from '@jsfsi-core/ts-nodejs';
321
+
322
+ // Initialize with module name (like NestJS Logger)
323
+ const logger = new GCPLogger('UserService');
324
+
325
+ // Sensitive keys are automatically redacted
326
+ logger.log('User login attempt', {
327
+ userId: '123',
328
+ email: 'user@example.com',
329
+ password: 'secret123', // Will be redacted as [HIDDEN BY LOGGER]
330
+ token: 'abc123xyz', // Will be redacted as [HIDDEN BY LOGGER]
331
+ authorization: 'Bearer token', // Will be redacted as [HIDDEN BY LOGGER]
332
+ });
333
+
334
+ // Output: Sensitive fields are automatically sanitized
335
+ // {
336
+ // "severity": "INFO",
337
+ // "message": {
338
+ // "textPayload": "User login attempt",
339
+ // "metadata": {
340
+ // "userId": "123",
341
+ // "email": "user@example.com",
342
+ // "password": "[HIDDEN BY LOGGER]",
343
+ // "token": "[HIDDEN BY LOGGER]",
344
+ // "authorization": "[HIDDEN BY LOGGER]"
345
+ // }
346
+ // }
347
+ // }
348
+ ```
349
+
350
+ **Automatically redacted sensitive keys include:**
351
+
352
+ - `password`, `pass`, `psw`
353
+ - `token`, `access_token`
354
+ - `authorization`, `authentication`, `auth`
355
+ - `x-api-key`, `x-api-token`, `x-key`, `x-token`
356
+ - `cookie`
357
+ - `secret`, `client-secret`
358
+ - `credentials`
359
+
360
+ **Features:**
361
+
362
+ - ✅ Compatible with **NestJS LoggerService** interface - can be used directly in NestJS applications
363
+ - ✅ **Automatic data sanitization** - sensitive keys are automatically redacted
364
+ - ✅ **Structured logging** - logs formatted for Google Cloud Platform
365
+ - ✅ **Safe stringification** - handles circular references safely
366
+ - ✅ **Severity mapping** - maps log levels to GCP severity levels
367
+
368
+ #### Mock Logger
369
+
370
+ For testing:
371
+
372
+ ```typescript
373
+ import { MockLogger } from '@jsfsi-core/ts-nodejs';
374
+
375
+ const logger = new MockLogger();
376
+ logger.log('Hello world');
377
+
378
+ // Assertions
379
+ expect(logger.logs).toContainEqual({ level: 'log', message: 'Hello world' });
380
+ ```
381
+
382
+ ### Environment Loader
383
+
384
+ Type-safe environment variable loading:
385
+
386
+ ```typescript
387
+ import { loadEnv } from '@jsfsi-core/ts-nodejs';
388
+
389
+ // Load .env file
390
+ loadEnv();
391
+
392
+ // Access environment variables
393
+ const port = process.env.PORT;
394
+ const dbUrl = process.env.DATABASE_URL;
395
+ ```
396
+
397
+ **Note**: For type-safe configuration with validation, use `@jsfsi-core/ts-crossplatform`'s `parseConfig` with Zod schemas.
398
+
399
+ ## 📝 Naming Conventions
400
+
401
+ ### Repositories
402
+
403
+ - **Repositories**: PascalCase suffix with `Repository` (e.g., `UserRepository`, `OrderRepository`)
404
+ - **Methods**: Use descriptive names (`findById`, `save`, `delete`)
405
+
406
+ ### Entities
407
+
408
+ - **Entities**: PascalCase suffix with `Entity` (e.g., `UserEntity`, `OrderEntity`)
409
+
410
+ ### Services
411
+
412
+ - **Services**: PascalCase suffix with `Service` (e.g., `UserService`, `OrderService`)
413
+
414
+ ## 🧪 Testing Principles
415
+
416
+ ### Testing Repositories
417
+
418
+ Use `TransactionalRepositoryMock` for testing:
419
+
420
+ ```typescript
421
+ import { TransactionalRepositoryMock } from '@jsfsi-core/ts-nodejs';
422
+
423
+ describe('UserRepository', () => {
424
+ let repository: UserRepository;
425
+
426
+ beforeEach(() => {
427
+ const mockDataSource = {} as DataSource;
428
+ repository = new UserRepository(mockDataSource);
429
+ });
430
+
431
+ it('finds user by id', async () => {
432
+ const user = await repository.findById('123');
433
+ // Test implementation
434
+ });
435
+ });
436
+ ```
437
+
438
+ ### Testing with Transactions
439
+
440
+ ```typescript
441
+ describe('UserService', () => {
442
+ it('creates user within transaction', async () => {
443
+ const result = await userService.createUserWithProfile(userData, profileData);
444
+
445
+ // Verify both user and profile were created
446
+ expect(result.user).toBeDefined();
447
+ expect(result.profile).toBeDefined();
448
+ });
449
+ });
450
+ ```
451
+
452
+ ### Testing Logging
453
+
454
+ Use `MockLogger` for testing:
455
+
456
+ ```typescript
457
+ import { MockLogger } from '@jsfsi-core/ts-nodejs';
458
+
459
+ describe('UserService', () => {
460
+ let logger: MockLogger;
461
+ let service: UserService;
462
+
463
+ beforeEach(() => {
464
+ logger = new MockLogger();
465
+ service = new UserService(logger);
466
+ });
467
+
468
+ it('logs error on failure', async () => {
469
+ await service.processOrder('invalid-id');
470
+
471
+ expect(logger.error).toHaveBeenCalled();
472
+ expect(logger.error).toHaveBeenCalledWith(
473
+ expect.stringContaining('Failed'),
474
+ expect.any(Object),
475
+ );
476
+ });
477
+ });
478
+ ```
479
+
480
+ ## ⚠️ Error Handling Principles
481
+
482
+ ### Result Types in Repository Methods
483
+
484
+ **Repositories should return Result types** when operations can fail:
485
+
486
+ ```typescript
487
+ import { Result, Ok, Fail, isFailure } from '@jsfsi-core/ts-crossplatform';
488
+
489
+ // ✅ Good - Return Result type
490
+ async findById(id: string): Promise<Result<UserEntity, UserNotFoundFailure>> {
491
+ const repository = this.getRepository(UserEntity);
492
+ const user = await repository.findOne({ where: { id } });
493
+
494
+ if (!user) {
495
+ return Fail(new UserNotFoundFailure(id));
496
+ }
497
+
498
+ return Ok(user);
499
+ }
500
+
501
+ // ❌ Bad - Throwing exceptions
502
+ async findById(id: string): Promise<UserEntity> {
503
+ const repository = this.getRepository(UserEntity);
504
+ const user = await repository.findOne({ where: { id } });
505
+
506
+ if (!user) {
507
+ throw new Error('User not found'); // Don't throw in repository
508
+ }
509
+
510
+ return user;
511
+ }
512
+ ```
513
+
514
+ ### Transaction Error Handling
515
+
516
+ Transactions automatically rollback on errors:
517
+
518
+ ```typescript
519
+ async function createUserWithProfile(
520
+ userData: CreateUserData,
521
+ profileData: CreateProfileData,
522
+ ): Promise<Result<User, CreateUserFailure>> {
523
+ return this.userRepository.withTransaction(async (userRepo) => {
524
+ const [user, userFailure] = await userRepo.save(userData);
525
+
526
+ if (isFailure(CreateUserFailure)(userFailure)) {
527
+ // Transaction automatically rolls back
528
+ return Fail(userFailure);
529
+ }
530
+
531
+ const [profile, profileFailure] = await this.profileRepository
532
+ .withRepositoryManager(userRepo)
533
+ .save(profileData);
534
+
535
+ if (isFailure(CreateProfileFailure)(profileFailure)) {
536
+ // Transaction automatically rolls back
537
+ return Fail(profileFailure);
538
+ }
539
+
540
+ return Ok({ user, profile });
541
+ });
542
+ }
543
+ ```
544
+
545
+ ### Try-Catch at Edges
546
+
547
+ **Try-catch should only be used at the edge** (when interfacing with external systems):
548
+
549
+ ```typescript
550
+ // ✅ Good - In adapter (edge)
551
+ export class DatabaseAdapter implements IUserRepository {
552
+ async save(user: UserEntity): Promise<Result<UserEntity, DatabaseFailure>> {
553
+ try {
554
+ const saved = await this.repository.save(user);
555
+ return Ok(saved);
556
+ } catch (error) {
557
+ return Fail(new DatabaseFailure(error));
558
+ }
559
+ }
560
+ }
561
+
562
+ // ✅ Good - Domain service (no try-catch)
563
+ export class UserService {
564
+ async createUser(data: CreateUserData): Promise<Result<User, CreateUserFailure>> {
565
+ // No try-catch - errors are handled as Result types
566
+ return this.userRepository.save(data);
567
+ }
568
+ }
569
+ ```
570
+
571
+ ## 🎯 Domain-Driven Design
572
+
573
+ ### Repository Pattern
574
+
575
+ Repositories abstract database access:
576
+
577
+ ```typescript
578
+ // Domain interface
579
+ export interface IUserRepository {
580
+ findById(id: string): Promise<Result<User, UserNotFoundFailure>>;
581
+ save(user: User): Promise<Result<User, SaveUserFailure>>;
582
+ }
583
+
584
+ // Implementation in adapter
585
+ export class UserRepository extends TransactionalRepository implements IUserRepository {
586
+ async findById(id: string): Promise<Result<User, UserNotFoundFailure>> {
587
+ const repository = this.getRepository(UserEntity);
588
+ const entity = await repository.findOne({ where: { id } });
589
+
590
+ if (!entity) {
591
+ return Fail(new UserNotFoundFailure(id));
592
+ }
593
+
594
+ return Ok(mapEntityToDomain(entity));
595
+ }
596
+ }
597
+ ```
598
+
599
+ ### Entity Mapping
600
+
601
+ Map between database entities and domain models:
602
+
603
+ ```typescript
604
+ // Domain model
605
+ export type User = {
606
+ id: string;
607
+ email: string;
608
+ name: string;
609
+ };
610
+
611
+ // Database entity
612
+ @Entity('users')
613
+ export class UserEntity {
614
+ @PrimaryColumn('uuid')
615
+ id: string;
616
+
617
+ @Column()
618
+ email: string;
619
+
620
+ @Column()
621
+ name: string;
622
+ }
623
+
624
+ // Mapping functions
625
+ function mapEntityToDomain(entity: UserEntity): User {
626
+ return {
627
+ id: entity.id,
628
+ email: entity.email,
629
+ name: entity.name,
630
+ };
631
+ }
632
+
633
+ function mapDomainToEntity(user: User): UserEntity {
634
+ const entity = new UserEntity();
635
+ entity.id = user.id;
636
+ entity.email = user.email;
637
+ entity.name = user.name;
638
+ return entity;
639
+ }
640
+ ```
641
+
642
+ ## 🔄 Result Class Integration
643
+
644
+ ### Repository Methods
645
+
646
+ Always return Result types from repository methods:
647
+
648
+ ```typescript
649
+ export class UserRepository extends TransactionalRepository {
650
+ async findById(id: string): Promise<Result<UserEntity, UserNotFoundFailure>> {
651
+ const repository = this.getRepository(UserEntity);
652
+ const user = await repository.findOne({ where: { id } });
653
+
654
+ if (!user) {
655
+ return Fail(new UserNotFoundFailure(id));
656
+ }
657
+
658
+ return Ok(user);
659
+ }
660
+
661
+ async save(user: UserEntity): Promise<Result<UserEntity, SaveUserFailure>> {
662
+ try {
663
+ const repository = this.getRepository(UserEntity);
664
+ const saved = await repository.save(user);
665
+ return Ok(saved);
666
+ } catch (error) {
667
+ return Fail(new SaveUserFailure(error));
668
+ }
669
+ }
670
+ }
671
+ ```
672
+
673
+ ### Service Layer
674
+
675
+ Chain Result types in service layer:
676
+
677
+ ```typescript
678
+ export class UserService {
679
+ async createUser(data: CreateUserData): Promise<Result<User, CreateUserFailure>> {
680
+ // Validate first
681
+ const [validated, validationFailure] = validateUserData(data);
682
+ if (isFailure(ValidationFailure)(validationFailure)) {
683
+ return Fail(validationFailure);
684
+ }
685
+
686
+ // Save to database
687
+ const [user, saveFailure] = await this.userRepository.save(validated);
688
+ if (isFailure(SaveUserFailure)(saveFailure)) {
689
+ return Fail(saveFailure);
690
+ }
691
+
692
+ return Ok(user);
693
+ }
694
+ }
695
+ ```
696
+
697
+ ## 📚 Best Practices
698
+
699
+ ### 1. Transaction Boundaries
700
+
701
+ Keep transactions as short as possible:
702
+
703
+ ```typescript
704
+ // ✅ Good - Short transaction
705
+ async function createUser(data: CreateUserData): Promise<Result<User>> {
706
+ return this.repository.withTransaction(async (repo) => {
707
+ return repo.save(data);
708
+ });
709
+ }
710
+
711
+ // ❌ Bad - Long transaction with external calls
712
+ async function createUser(data: CreateUserData): Promise<Result<User>> {
713
+ return this.repository.withTransaction(async (repo) => {
714
+ const user = await repo.save(data);
715
+ await this.emailService.sendWelcomeEmail(user.email); // Don't do this in transaction
716
+ await this.cacheService.invalidate(user.id); // Don't do this in transaction
717
+ return Ok(user);
718
+ });
719
+ }
720
+ ```
721
+
722
+ ### 2. Repository Methods
723
+
724
+ Keep repository methods focused on data access:
725
+
726
+ ```typescript
727
+ // ✅ Good - Focused data access
728
+ async findById(id: string): Promise<Result<UserEntity, UserNotFoundFailure>> {
729
+ const repository = this.getRepository(UserEntity);
730
+ const user = await repository.findOne({ where: { id } });
731
+ return user ? Ok(user) : Fail(new UserNotFoundFailure(id));
732
+ }
733
+
734
+ // ❌ Bad - Business logic in repository
735
+ async findById(id: string): Promise<Result<UserEntity, UserNotFoundFailure>> {
736
+ const repository = this.getRepository(UserEntity);
737
+ const user = await repository.findOne({ where: { id } });
738
+
739
+ // Don't put business logic here
740
+ if (user && user.isActive) {
741
+ user.lastAccessed = new Date();
742
+ await repository.save(user);
743
+ }
744
+
745
+ return user ? Ok(user) : Fail(new UserNotFoundFailure(id));
746
+ }
747
+ ```
748
+
749
+ ### 3. Error Handling
750
+
751
+ Use Result types, not exceptions:
752
+
753
+ ```typescript
754
+ // ✅ Good
755
+ async findById(id: string): Promise<Result<UserEntity, UserNotFoundFailure>> {
756
+ const user = await this.getRepository(UserEntity).findOne({ where: { id } });
757
+ return user ? Ok(user) : Fail(new UserNotFoundFailure(id));
758
+ }
759
+
760
+ // ❌ Bad
761
+ async findById(id: string): Promise<UserEntity> {
762
+ const user = await this.getRepository(UserEntity).findOne({ where: { id } });
763
+ if (!user) {
764
+ throw new Error('User not found');
765
+ }
766
+ return user;
767
+ }
768
+ ```
769
+
770
+ ### 4. Logging
771
+
772
+ Use structured logging:
773
+
774
+ ```typescript
775
+ // ✅ Good - Structured logging
776
+ this.logger.log('User created', { userId: user.id, email: user.email });
777
+
778
+ // ❌ Bad - String interpolation
779
+ this.logger.log(`User ${user.id} created with email ${user.email}`);
780
+ ```
781
+
782
+ ### 5. Environment Variables
783
+
784
+ Use type-safe configuration:
785
+
786
+ ```typescript
787
+ // ✅ Good - Type-safe with Zod
788
+ import { parseConfig } from '@jsfsi-core/ts-crossplatform';
789
+ import { z } from 'zod';
790
+
791
+ const ConfigSchema = z.object({
792
+ DATABASE_URL: z.string().url(),
793
+ PORT: z.string().transform(Number),
794
+ });
795
+
796
+ export const config = parseConfig(ConfigSchema);
797
+
798
+ // ❌ Bad - Direct environment access
799
+ const dbUrl = process.env.DATABASE_URL; // Not type-safe
800
+ ```
801
+
802
+ ## 🔗 Additional Resources
803
+
804
+ ### TypeORM
805
+
806
+ - [TypeORM Documentation](https://typeorm.io/)
807
+ - [TypeORM Transactions](https://typeorm.io/transactions)
808
+
809
+ ### Architecture
810
+
811
+ - [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)
812
+ - [Domain-Driven Design](https://www.domainlanguage.com/ddd/)
813
+
814
+ ### Error Handling
815
+
816
+ - [Result Type Pattern](https://enterprisecraftsmanship.com/posts/functional-c-handling-failures-input-errors/)
817
+
818
+ ## 📄 License
819
+
820
+ ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsfsi-core/ts-nodejs",
3
- "version": "1.1.2",
3
+ "version": "1.1.6",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",