@groundbrick/service-base 0.0.1
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 +713 -0
- package/dist/core/BaseService.d.ts +55 -0
- package/dist/core/BaseService.d.ts.map +1 -0
- package/dist/core/BaseService.js +123 -0
- package/dist/core/BaseService.js.map +1 -0
- package/dist/email/EmailConfigurationService.d.ts +14 -0
- package/dist/email/EmailConfigurationService.d.ts.map +1 -0
- package/dist/email/EmailConfigurationService.js +60 -0
- package/dist/email/EmailConfigurationService.js.map +1 -0
- package/dist/email/EmailService.d.ts +15 -0
- package/dist/email/EmailService.d.ts.map +1 -0
- package/dist/email/EmailService.js +96 -0
- package/dist/email/EmailService.js.map +1 -0
- package/dist/email/interfaces/EmailInterfaces.d.ts +45 -0
- package/dist/email/interfaces/EmailInterfaces.d.ts.map +1 -0
- package/dist/email/interfaces/EmailInterfaces.js +2 -0
- package/dist/email/interfaces/EmailInterfaces.js.map +1 -0
- package/dist/email/providers/SmtpEmailProvider.d.ts +14 -0
- package/dist/email/providers/SmtpEmailProvider.d.ts.map +1 -0
- package/dist/email/providers/SmtpEmailProvider.js +88 -0
- package/dist/email/providers/SmtpEmailProvider.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/types/BusinessError.d.ts +31 -0
- package/dist/types/BusinessError.d.ts.map +1 -0
- package/dist/types/BusinessError.js +57 -0
- package/dist/types/BusinessError.js.map +1 -0
- package/dist/types/ServiceOptions.d.ts +14 -0
- package/dist/types/ServiceOptions.d.ts.map +1 -0
- package/dist/types/ServiceOptions.js +2 -0
- package/dist/types/ServiceOptions.js.map +1 -0
- package/dist/types/ServiceResult.d.ts +31 -0
- package/dist/types/ServiceResult.d.ts.map +1 -0
- package/dist/types/ServiceResult.js +61 -0
- package/dist/types/ServiceResult.js.map +1 -0
- package/dist/validation/ValidationError.d.ts +14 -0
- package/dist/validation/ValidationError.d.ts.map +1 -0
- package/dist/validation/ValidationError.js +2 -0
- package/dist/validation/ValidationError.js.map +1 -0
- package/dist/validation/ValidationHelper.d.ts +70 -0
- package/dist/validation/ValidationHelper.d.ts.map +1 -0
- package/dist/validation/ValidationHelper.js +169 -0
- package/dist/validation/ValidationHelper.js.map +1 -0
- package/dist/validation/ValidationResult.d.ts +12 -0
- package/dist/validation/ValidationResult.d.ts.map +1 -0
- package/dist/validation/ValidationResult.js +2 -0
- package/dist/validation/ValidationResult.js.map +1 -0
- package/dist/validation/Validator.d.ts +35 -0
- package/dist/validation/Validator.d.ts.map +1 -0
- package/dist/validation/Validator.js +52 -0
- package/dist/validation/Validator.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
# @groundbrick/service-base
|
|
2
|
+
|
|
3
|
+
🧠 Service layer foundation with validation, logging, error handling, and transaction support.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Service layer base classes and utilities for business logic implementation in layered architecture applications.
|
|
7
|
+
|
|
8
|
+
## 🚀 Features
|
|
9
|
+
|
|
10
|
+
### 🎯 **BaseService Class**
|
|
11
|
+
- Integrated logging with service context
|
|
12
|
+
- Database transaction management
|
|
13
|
+
- Input validation framework
|
|
14
|
+
- Standardized error handling
|
|
15
|
+
- Repository integration patterns
|
|
16
|
+
|
|
17
|
+
### 🔧 **Business Logic Patterns**
|
|
18
|
+
- Input validation before repository calls
|
|
19
|
+
- Business rule enforcement
|
|
20
|
+
- Cross-entity operations coordination
|
|
21
|
+
- Response transformation and mapping
|
|
22
|
+
|
|
23
|
+
### ⚡ **Error Handling**
|
|
24
|
+
- Business-specific error types (`BusinessError`)
|
|
25
|
+
- Validation error aggregation
|
|
26
|
+
- Repository error transformation
|
|
27
|
+
- Structured error responses for APIs
|
|
28
|
+
|
|
29
|
+
### ✅ **Validation System**
|
|
30
|
+
- Simple, extensible validation framework
|
|
31
|
+
- Common validation rules included
|
|
32
|
+
- Custom rule registration
|
|
33
|
+
- Field-level error reporting
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @groundbrick/service-base
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
This package requires:
|
|
44
|
+
- `@groundbrick/logger` - Logging functionality
|
|
45
|
+
- `@groundbrick/db-core` - Database interfaces
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### 1. Basic Service Implementation
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { BaseService, BusinessError, ValidationHelper } from '@groundbrick/service-base';
|
|
53
|
+
import { UserRepository } from './UserRepository';
|
|
54
|
+
|
|
55
|
+
interface User {
|
|
56
|
+
id: number;
|
|
57
|
+
name: string;
|
|
58
|
+
email: string;
|
|
59
|
+
active: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CreateUserRequest {
|
|
63
|
+
name: string;
|
|
64
|
+
email: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class UserService extends BaseService {
|
|
68
|
+
constructor(private userRepository: UserRepository) {
|
|
69
|
+
super();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async createUser(userData: CreateUserRequest): Promise<User> {
|
|
73
|
+
const endTimer = this.startOperation('createUser');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// 1. Validate input
|
|
77
|
+
const validationResult = await this.validate(userData, {
|
|
78
|
+
name: [
|
|
79
|
+
ValidationHelper.required(),
|
|
80
|
+
ValidationHelper.minLength(2),
|
|
81
|
+
ValidationHelper.maxLength(100)
|
|
82
|
+
],
|
|
83
|
+
email: [
|
|
84
|
+
ValidationHelper.required(),
|
|
85
|
+
ValidationHelper.email()
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!validationResult.isValid) {
|
|
90
|
+
throw new BusinessError(
|
|
91
|
+
'Invalid user data',
|
|
92
|
+
'VALIDATION_FAILED',
|
|
93
|
+
undefined,
|
|
94
|
+
{ errors: validationResult.errors }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Business logic
|
|
99
|
+
const existingUser = await this.userRepository.findByEmail(userData.email);
|
|
100
|
+
if (existingUser) {
|
|
101
|
+
throw new BusinessError(
|
|
102
|
+
'User already exists',
|
|
103
|
+
'USER_EXISTS',
|
|
104
|
+
undefined,
|
|
105
|
+
{ email: userData.email }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Create user
|
|
110
|
+
const user = await this.userRepository.create({
|
|
111
|
+
name: userData.name,
|
|
112
|
+
email: userData.email.toLowerCase(),
|
|
113
|
+
active: true
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.logger.info('User created successfully', { userId: user.id });
|
|
117
|
+
return user;
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (BusinessError.isBusinessError(error)) {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
throw this.handleRepositoryError(error as Error);
|
|
124
|
+
} finally {
|
|
125
|
+
endTimer();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Using Transactions
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export class OrderService extends BaseService {
|
|
135
|
+
constructor(
|
|
136
|
+
private orderRepository: OrderRepository,
|
|
137
|
+
private inventoryRepository: InventoryRepository,
|
|
138
|
+
options?: ServiceOptions
|
|
139
|
+
) {
|
|
140
|
+
super(options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
|
|
144
|
+
// Use transaction for multi-repository operations
|
|
145
|
+
return await this.withTransaction(async (tx) => {
|
|
146
|
+
// Create order
|
|
147
|
+
const order = await this.orderRepository.createWithTransaction(tx, {
|
|
148
|
+
user_id: orderData.user_id,
|
|
149
|
+
total_amount: orderData.total
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Update inventory
|
|
153
|
+
for (const item of orderData.items) {
|
|
154
|
+
await this.inventoryRepository.decrementStockWithTransaction(
|
|
155
|
+
tx,
|
|
156
|
+
item.product_id,
|
|
157
|
+
item.quantity
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return order;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### 📋 Transaction pattern:
|
|
168
|
+
1. **Create Parent First, Use Generated ID**
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Inside withTransaction callback
|
|
172
|
+
const newOrder = await this.orderRepository.createWithTransaction(tx, {
|
|
173
|
+
user_id: orderData.user_id,
|
|
174
|
+
total_amount: totalAmount,
|
|
175
|
+
status: 'pending'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// newOrder.id is now available (auto-generated by database)
|
|
179
|
+
|
|
180
|
+
// Use the order ID for child records
|
|
181
|
+
for (const item of orderData.items) {
|
|
182
|
+
await this.orderItemRepository.createWithTransaction(tx, {
|
|
183
|
+
order_id: newOrder.id, // 👈 Use the generated ID
|
|
184
|
+
product_id: item.product_id,
|
|
185
|
+
quantity: item.quantity,
|
|
186
|
+
unit_price: item.unit_price
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
2. **Alternative: Bulk Creation**
|
|
192
|
+
```typescript
|
|
193
|
+
// Create all order items at once
|
|
194
|
+
const orderItemsData = orderData.items.map(item => ({
|
|
195
|
+
order_id: newOrder.id, // Same ID for all items
|
|
196
|
+
product_id: item.product_id,
|
|
197
|
+
quantity: item.quantity,
|
|
198
|
+
unit_price: item.unit_price
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
await this.orderItemRepository.createManyWithTransaction(tx, orderItemsData);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
3. **Complete Example with Proper Flow**
|
|
205
|
+
```typescript
|
|
206
|
+
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
|
|
207
|
+
return await this.withTransaction(async (tx) => {
|
|
208
|
+
// 1. Validate business rules first
|
|
209
|
+
await this.validateOrderData(orderData);
|
|
210
|
+
|
|
211
|
+
// 2. Calculate total amount
|
|
212
|
+
let totalAmount = 0;
|
|
213
|
+
const validatedItems = [];
|
|
214
|
+
|
|
215
|
+
for (const item of orderData.items) {
|
|
216
|
+
const product = await this.productRepository.findByIdWithTransaction(tx, item.product_id);
|
|
217
|
+
// ... validation logic
|
|
218
|
+
totalAmount += product.price * item.quantity;
|
|
219
|
+
validatedItems.push({
|
|
220
|
+
product_id: item.product_id,
|
|
221
|
+
quantity: item.quantity,
|
|
222
|
+
unit_price: product.price
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Create the order (gets auto-generated ID)
|
|
227
|
+
const newOrder = await this.orderRepository.createWithTransaction(tx, {
|
|
228
|
+
user_id: orderData.user_id,
|
|
229
|
+
total_amount: totalAmount,
|
|
230
|
+
status: 'pending',
|
|
231
|
+
created_at: new Date()
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 4. Create order items using the order ID
|
|
235
|
+
for (const item of validatedItems) {
|
|
236
|
+
await this.orderItemRepository.createWithTransaction(tx, {
|
|
237
|
+
order_id: newOrder.id, // 👈 Key: Use generated order ID
|
|
238
|
+
product_id: item.product_id,
|
|
239
|
+
quantity: item.quantity,
|
|
240
|
+
unit_price: item.unit_price
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 5. Update product inventory
|
|
245
|
+
for (const item of orderData.items) {
|
|
246
|
+
await this.productRepository.decrementStockWithTransaction(
|
|
247
|
+
tx,
|
|
248
|
+
item.product_id,
|
|
249
|
+
item.quantity
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 6. Return the complete order (with ID)
|
|
254
|
+
return newOrder;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### 🔧 Repository Method Requirements:
|
|
260
|
+
Your repositories need to support transaction-aware methods:
|
|
261
|
+
```typescript
|
|
262
|
+
// In your OrderRepository
|
|
263
|
+
class OrderRepository extends BaseRepository<Order> {
|
|
264
|
+
async createWithTransaction(tx: DatabaseTransaction, data: Partial<Order>): Promise<Order> {
|
|
265
|
+
// Use tx.query() instead of this.db.query()
|
|
266
|
+
const result = await tx.query(
|
|
267
|
+
'INSERT INTO orders (user_id, total_amount, status, created_at) VALUES (?, ?, ?, ?) RETURNING *',
|
|
268
|
+
[data.user_id, data.total_amount, data.status, data.created_at]
|
|
269
|
+
);
|
|
270
|
+
return result.rows[0];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// In your OrderItemRepository
|
|
275
|
+
class OrderItemRepository extends BaseRepository<OrderItem> {
|
|
276
|
+
async createWithTransaction(tx: DatabaseTransaction, data: Partial<OrderItem>): Promise<OrderItem> {
|
|
277
|
+
const result = await tx.query(
|
|
278
|
+
'INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?) RETURNING *',
|
|
279
|
+
[data.order_id, data.product_id, data.quantity, data.unit_price]
|
|
280
|
+
);
|
|
281
|
+
return result.rows[0];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async createManyWithTransaction(tx: DatabaseTransaction, items: Partial<OrderItem>[]): Promise<OrderItem[]> {
|
|
285
|
+
// Bulk insert implementation
|
|
286
|
+
const values = items.map(item => [item.order_id, item.product_id, item.quantity, item.unit_price]);
|
|
287
|
+
// ... bulk insert logic
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
#### 💡 Key Points:
|
|
292
|
+
|
|
293
|
+
- Order matters: Create parent record first to get the ID
|
|
294
|
+
- Use the transaction: All operations must use the same tx parameter
|
|
295
|
+
- ID is immediately available: After createWithTransaction, the returned object has the generated ID
|
|
296
|
+
- All-or-nothing: If any step fails, the entire transaction rolls back
|
|
297
|
+
- Performance: Consider bulk operations for multiple child records
|
|
298
|
+
|
|
299
|
+
### 3. Custom Validation Rules
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
export class ProductService extends BaseService {
|
|
303
|
+
constructor(private productRepository: ProductRepository) {
|
|
304
|
+
super();
|
|
305
|
+
|
|
306
|
+
// Register custom validation rules
|
|
307
|
+
this.validator.registerRule('sku', (value) => {
|
|
308
|
+
const skuPattern = /^[A-Z]{2}-\d{4}$/;
|
|
309
|
+
return skuPattern.test(value) ? null : 'SKU must follow format: XX-0000';
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async createProduct(productData: CreateProductRequest): Promise<Product> {
|
|
314
|
+
const validationResult = await this.validate(productData, {
|
|
315
|
+
name: [ValidationHelper.required(), ValidationHelper.minLength(2)],
|
|
316
|
+
sku: [ValidationHelper.required(), this.validator.getRule('sku')!],
|
|
317
|
+
price: [ValidationHelper.required(), ValidationHelper.numberRange(0.01, 10000)]
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!validationResult.isValid) {
|
|
321
|
+
throw new BusinessError('Invalid product data', 'VALIDATION_FAILED', undefined, {
|
|
322
|
+
errors: validationResult.errors
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create product logic...
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## API Reference
|
|
332
|
+
|
|
333
|
+
### BaseService
|
|
334
|
+
|
|
335
|
+
The abstract base class that your services should extend.
|
|
336
|
+
|
|
337
|
+
#### Constructor Options
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
interface ServiceOptions {
|
|
341
|
+
database?: DatabaseClient; // For transaction support
|
|
342
|
+
validator?: Validator; // Custom validator instance
|
|
343
|
+
config?: Record<string, any>; // Service-specific config
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### Protected Methods
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// Validation
|
|
351
|
+
protected async validate<T>(data: T, rules: ValidationRules): Promise<ValidationResult>
|
|
352
|
+
protected validateRequired(data: Record<string, any>, fields: string[]): void
|
|
353
|
+
|
|
354
|
+
// Transactions
|
|
355
|
+
protected async withTransaction<T>(callback: (tx: DatabaseTransaction) => Promise<T>): Promise<T>
|
|
356
|
+
|
|
357
|
+
// Error Handling
|
|
358
|
+
protected handleRepositoryError(error: Error): BusinessError
|
|
359
|
+
|
|
360
|
+
// Utilities
|
|
361
|
+
protected startOperation(operation: string, metadata?: Record<string, any>): () => void
|
|
362
|
+
protected safeSerialize(obj: any): any
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### 🛡 BusinessError
|
|
366
|
+
|
|
367
|
+
Custom error class for business logic errors.
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
class BusinessError extends Error {
|
|
371
|
+
constructor(
|
|
372
|
+
message: string,
|
|
373
|
+
code: string,
|
|
374
|
+
originalError?: Error,
|
|
375
|
+
context?: Record<string, any>
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Properties
|
|
379
|
+
readonly code: string
|
|
380
|
+
readonly originalError?: Error
|
|
381
|
+
readonly context?: Record<string, any>
|
|
382
|
+
readonly timestamp: Date
|
|
383
|
+
|
|
384
|
+
// Methods
|
|
385
|
+
toJSON(): Record<string, any>
|
|
386
|
+
toUserResponse(): { error: string; code: string; timestamp: string }
|
|
387
|
+
hasCode(code: string): boolean
|
|
388
|
+
static isBusinessError(error: any): error is BusinessError
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### ValidationHelper
|
|
393
|
+
|
|
394
|
+
Pre-built validation rules for common scenarios.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
class ValidationHelper {
|
|
398
|
+
// Basic rules
|
|
399
|
+
static required(): ValidationRule
|
|
400
|
+
static minLength(min: number): ValidationRule
|
|
401
|
+
static maxLength(max: number): ValidationRule
|
|
402
|
+
static email(): ValidationRule
|
|
403
|
+
static numberRange(min?: number, max?: number): ValidationRule
|
|
404
|
+
static pattern(regex: RegExp, message: string): ValidationRule
|
|
405
|
+
static oneOf(options: any[], message?: string): ValidationRule
|
|
406
|
+
|
|
407
|
+
// Array rules
|
|
408
|
+
static arrayMinLength(min: number): ValidationRule
|
|
409
|
+
|
|
410
|
+
// Advanced rules
|
|
411
|
+
static custom(validator: (value: any, data?: any) => string | null): ValidationRule
|
|
412
|
+
static when(condition: (data: any) => boolean, rule: ValidationRule): ValidationRule
|
|
413
|
+
|
|
414
|
+
// Utility methods
|
|
415
|
+
static combine(...rules: ValidationRule[]): ValidationRule[]
|
|
416
|
+
static entityId(): ValidationRule[]
|
|
417
|
+
|
|
418
|
+
// Pre-configured rule sets
|
|
419
|
+
static userCreation(): ValidationRules
|
|
420
|
+
static pagination(): ValidationRules
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## ✅ Validation System
|
|
425
|
+
|
|
426
|
+
### Basic Validation
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
const result = await this.validate(data, {
|
|
430
|
+
email: [ValidationHelper.required(), ValidationHelper.email()],
|
|
431
|
+
age: [ValidationHelper.numberRange(18, 120)]
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!result.isValid) {
|
|
435
|
+
// Handle validation errors
|
|
436
|
+
console.log(result.errors); // { email: ['Must be a valid email'], age: ['Must be at least 18'] }
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### 🧠 Custom Validation Rules
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// Register a custom rule
|
|
444
|
+
this.validator.registerRule('phone', (value) => {
|
|
445
|
+
const phonePattern = /^\+?[\d\s-()]{10,}$/;
|
|
446
|
+
return phonePattern.test(value) ? null : 'Invalid phone number format';
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Use in validation
|
|
450
|
+
const rules = {
|
|
451
|
+
phone: [ValidationHelper.required(), this.validator.getRule('phone')!]
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Conditional Validation
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
const rules = {
|
|
459
|
+
email: [ValidationHelper.required(), ValidationHelper.email()],
|
|
460
|
+
password: ValidationHelper.when(
|
|
461
|
+
(data) => data.isNewUser === true,
|
|
462
|
+
ValidationHelper.combine(
|
|
463
|
+
ValidationHelper.required(),
|
|
464
|
+
ValidationHelper.minLength(8)
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
};
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Error Handling Patterns
|
|
471
|
+
|
|
472
|
+
### 1. Repository Error Transformation
|
|
473
|
+
|
|
474
|
+
The `handleRepositoryError` method automatically transforms common database errors:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// Database constraint violation → BusinessError with DUPLICATE_RESOURCE code
|
|
478
|
+
// Record not found → BusinessError with RESOURCE_NOT_FOUND code
|
|
479
|
+
// Foreign key violation → BusinessError with CONSTRAINT_VIOLATION code
|
|
480
|
+
// Other errors → BusinessError with OPERATION_FAILED code
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 2. Structured Error Responses
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
try {
|
|
487
|
+
const user = await userService.createUser(userData);
|
|
488
|
+
return createSuccessResult(user);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (BusinessError.isBusinessError(error)) {
|
|
491
|
+
// Handle business logic errors
|
|
492
|
+
return createErrorResult(error);
|
|
493
|
+
}
|
|
494
|
+
// Handle unexpected errors
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 3. API Integration
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
export class UserController {
|
|
503
|
+
async createUser(req: any, res: any) {
|
|
504
|
+
try {
|
|
505
|
+
const user = await this.userService.createUser(req.body);
|
|
506
|
+
res.status(201).json(createSuccessResult(user));
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const result = createErrorResult(error as Error);
|
|
509
|
+
|
|
510
|
+
// Map business error codes to HTTP status codes
|
|
511
|
+
let status = 500;
|
|
512
|
+
if (BusinessError.isBusinessError(error)) {
|
|
513
|
+
switch (error.code) {
|
|
514
|
+
case 'VALIDATION_FAILED': status = 400; break;
|
|
515
|
+
case 'USER_EXISTS': status = 409; break;
|
|
516
|
+
case 'USER_NOT_FOUND': status = 404; break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
res.status(status).json(result);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Transaction Management
|
|
527
|
+
|
|
528
|
+
### Simple Transaction
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
async updateUserProfile(userId: number, profileData: any): Promise<User> {
|
|
532
|
+
return await this.withTransaction(async (tx) => {
|
|
533
|
+
// Update user
|
|
534
|
+
const user = await this.userRepository.updateWithTransaction(tx, userId, {
|
|
535
|
+
name: profileData.name,
|
|
536
|
+
email: profileData.email
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Update user preferences
|
|
540
|
+
await this.preferencesRepository.updateWithTransaction(tx, userId, {
|
|
541
|
+
theme: profileData.theme,
|
|
542
|
+
language: profileData.language
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return user;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Complex Business Transaction
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
async processOrder(orderData: CreateOrderRequest): Promise<Order> {
|
|
554
|
+
return await this.withTransaction(async (tx) => {
|
|
555
|
+
// 1. Validate inventory
|
|
556
|
+
for (const item of orderData.items) {
|
|
557
|
+
const product = await this.productRepository.findByIdWithTransaction(tx, item.productId);
|
|
558
|
+
if (product.stock < item.quantity) {
|
|
559
|
+
throw new BusinessError('Insufficient stock', 'INSUFFICIENT_STOCK');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 2. Create order
|
|
564
|
+
const order = await this.orderRepository.createWithTransaction(tx, orderData);
|
|
565
|
+
|
|
566
|
+
// 3. Update inventory
|
|
567
|
+
for (const item of orderData.items) {
|
|
568
|
+
await this.productRepository.decrementStockWithTransaction(
|
|
569
|
+
tx, item.productId, item.quantity
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 4. Send notification (example of non-transactional side effect)
|
|
574
|
+
// Note: This should be handled outside the transaction
|
|
575
|
+
// Consider using event-driven patterns for side effects
|
|
576
|
+
|
|
577
|
+
return order;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Best Practices
|
|
583
|
+
|
|
584
|
+
### 1. Keep Services Focused
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// ✅ Good - Single responsibility
|
|
588
|
+
class UserService extends BaseService {
|
|
589
|
+
async createUser(userData: CreateUserRequest): Promise<User> { }
|
|
590
|
+
async updateUser(id: number, updates: UpdateUserRequest): Promise<User> { }
|
|
591
|
+
async getUserById(id: number): Promise<User> { }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ❌ Bad - Too many responsibilities
|
|
595
|
+
class UserService extends BaseService {
|
|
596
|
+
async createUser(userData: CreateUserRequest): Promise<User> { }
|
|
597
|
+
async sendWelcomeEmail(user: User): Promise<void> { } // Should be EmailService
|
|
598
|
+
async generateReport(userId: number): Promise<Report> { } // Should be ReportService
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### 2. Validate Input Early
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// ✅ Good - Validate first
|
|
606
|
+
async createUser(userData: CreateUserRequest): Promise<User> {
|
|
607
|
+
const validationResult = await this.validate(userData, this.getUserValidationRules());
|
|
608
|
+
if (!validationResult.isValid) {
|
|
609
|
+
throw new BusinessError('Validation failed', 'VALIDATION_FAILED', undefined, {
|
|
610
|
+
errors: validationResult.errors
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Continue with business logic...
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ❌ Bad - Validation mixed with business logic
|
|
618
|
+
async createUser(userData: CreateUserRequest): Promise<User> {
|
|
619
|
+
const existingUser = await this.userRepository.findByEmail(userData.email);
|
|
620
|
+
if (!userData.email) { // Too late for basic validation
|
|
621
|
+
throw new Error('Email required');
|
|
622
|
+
}
|
|
623
|
+
// ...
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### 3. Use Transactions for Multi-Repository Operations
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// ✅ Good - Transaction for consistency
|
|
631
|
+
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
|
|
632
|
+
await this.withTransaction(async (tx) => {
|
|
633
|
+
await this.accountRepository.decrementBalanceWithTransaction(tx, fromAccount, amount);
|
|
634
|
+
await this.accountRepository.incrementBalanceWithTransaction(tx, toAccount, amount);
|
|
635
|
+
await this.transactionRepository.createWithTransaction(tx, {
|
|
636
|
+
from_account: fromAccount,
|
|
637
|
+
to_account: toAccount,
|
|
638
|
+
amount
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ❌ Bad - No transaction, inconsistent state possible
|
|
644
|
+
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
|
|
645
|
+
await this.accountRepository.decrementBalance(fromAccount, amount);
|
|
646
|
+
await this.accountRepository.incrementBalance(toAccount, amount); // Could fail, leaving inconsistent state
|
|
647
|
+
await this.transactionRepository.create({ from_account: fromAccount, to_account: toAccount, amount });
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### 4. Handle Errors Appropriately
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
// ✅ Good - Structured error handling
|
|
655
|
+
async getUserById(id: number): Promise<User> {
|
|
656
|
+
try {
|
|
657
|
+
this.validateRequired({ id }, ['id']);
|
|
658
|
+
|
|
659
|
+
const user = await this.userRepository.findById(id);
|
|
660
|
+
if (!user) {
|
|
661
|
+
throw new BusinessError('User not found', 'USER_NOT_FOUND', undefined, { userId: id });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return user;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (BusinessError.isBusinessError(error)) {
|
|
667
|
+
throw error; // Re-throw business errors
|
|
668
|
+
}
|
|
669
|
+
// Transform repository errors
|
|
670
|
+
throw this.handleRepositoryError(error as Error);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
## Integration with Other Packages
|
|
676
|
+
|
|
677
|
+
This service layer integrates seamlessly with other microframework packages:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
import { createLogger } from '@groundbrick/logger';
|
|
681
|
+
import { DatabaseFactory } from '@groundbrick/db-postgres';
|
|
682
|
+
import { UserRepository } from '@groundbrick/repository-base';
|
|
683
|
+
import { UserService } from './UserService';
|
|
684
|
+
|
|
685
|
+
// Initialize database
|
|
686
|
+
const db = DatabaseFactory.getInstance({
|
|
687
|
+
host: 'localhost',
|
|
688
|
+
database: 'myapp',
|
|
689
|
+
user: 'user',
|
|
690
|
+
password: 'password'
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Initialize repository
|
|
694
|
+
const userRepository = new UserRepository(db);
|
|
695
|
+
|
|
696
|
+
// Initialize service with database for transaction support
|
|
697
|
+
const userService = new UserService(userRepository, { database: db });
|
|
698
|
+
|
|
699
|
+
// Use in application
|
|
700
|
+
const user = await userService.createUser({
|
|
701
|
+
name: 'John Doe',
|
|
702
|
+
email: 'john@example.com'
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
## 📜 License
|
|
708
|
+
|
|
709
|
+
MIT
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
|