🚨 Shai-Hulud Strikes Again:834 Packages Compromised.Technical Analysis
Socket
Book a DemoInstallSign in
Socket

@groundbrick/service-base

Package Overview
Dependencies
Maintainers
1
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@groundbrick/service-base

Service layer base classes with validation, error handling, business logic patterns, and generic email infrastructure

latest
Source
npmnpm
Version
0.0.1
Version published
Maintainers
1
Created
Source

@groundbrick/service-base

🧠 Service layer foundation with validation, logging, error handling, and transaction support.

Service layer base classes and utilities for business logic implementation in layered architecture applications.

🚀 Features

🎯 BaseService Class

  • Integrated logging with service context
  • Database transaction management
  • Input validation framework
  • Standardized error handling
  • Repository integration patterns

🔧 Business Logic Patterns

  • Input validation before repository calls
  • Business rule enforcement
  • Cross-entity operations coordination
  • Response transformation and mapping

Error Handling

  • Business-specific error types (BusinessError)
  • Validation error aggregation
  • Repository error transformation
  • Structured error responses for APIs

Validation System

  • Simple, extensible validation framework
  • Common validation rules included
  • Custom rule registration
  • Field-level error reporting

Installation

npm install @groundbrick/service-base

Dependencies

This package requires:

  • @groundbrick/logger - Logging functionality
  • @groundbrick/db-core - Database interfaces

Quick Start

1. Basic Service Implementation

import { BaseService, BusinessError, ValidationHelper } from '@groundbrick/service-base';
import { UserRepository } from './UserRepository';

interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

export class UserService extends BaseService {
  constructor(private userRepository: UserRepository) {
    super();
  }

  async createUser(userData: CreateUserRequest): Promise<User> {
    const endTimer = this.startOperation('createUser');

    try {
      // 1. Validate input
      const validationResult = await this.validate(userData, {
        name: [
          ValidationHelper.required(),
          ValidationHelper.minLength(2),
          ValidationHelper.maxLength(100)
        ],
        email: [
          ValidationHelper.required(),
          ValidationHelper.email()
        ]
      });

      if (!validationResult.isValid) {
        throw new BusinessError(
          'Invalid user data',
          'VALIDATION_FAILED',
          undefined,
          { errors: validationResult.errors }
        );
      }

      // 2. Business logic
      const existingUser = await this.userRepository.findByEmail(userData.email);
      if (existingUser) {
        throw new BusinessError(
          'User already exists',
          'USER_EXISTS',
          undefined,
          { email: userData.email }
        );
      }

      // 3. Create user
      const user = await this.userRepository.create({
        name: userData.name,
        email: userData.email.toLowerCase(),
        active: true
      });

      this.logger.info('User created successfully', { userId: user.id });
      return user;

    } catch (error) {
      if (BusinessError.isBusinessError(error)) {
        throw error;
      }
      throw this.handleRepositoryError(error as Error);
    } finally {
      endTimer();
    }
  }
}

2. Using Transactions

export class OrderService extends BaseService {
  constructor(
    private orderRepository: OrderRepository,
    private inventoryRepository: InventoryRepository,
    options?: ServiceOptions
  ) {
    super(options);
  }

  async createOrder(orderData: CreateOrderRequest): Promise<Order> {
    // Use transaction for multi-repository operations
    return await this.withTransaction(async (tx) => {
      // Create order
      const order = await this.orderRepository.createWithTransaction(tx, {
        user_id: orderData.user_id,
        total_amount: orderData.total
      });

      // Update inventory
      for (const item of orderData.items) {
        await this.inventoryRepository.decrementStockWithTransaction(
          tx,
          item.product_id,
          item.quantity
        );
      }

      return order;
    });
  }
}

📋 Transaction pattern:

  • Create Parent First, Use Generated ID
// Inside withTransaction callback
const newOrder = await this.orderRepository.createWithTransaction(tx, {
  user_id: orderData.user_id,
  total_amount: totalAmount,
  status: 'pending'
});

// newOrder.id is now available (auto-generated by database)

// Use the order ID for child records
for (const item of orderData.items) {
  await this.orderItemRepository.createWithTransaction(tx, {
    order_id: newOrder.id,  // 👈 Use the generated ID
    product_id: item.product_id,
    quantity: item.quantity,
    unit_price: item.unit_price
  });
}
  • Alternative: Bulk Creation
// Create all order items at once
const orderItemsData = orderData.items.map(item => ({
  order_id: newOrder.id,  // Same ID for all items
  product_id: item.product_id,
  quantity: item.quantity,
  unit_price: item.unit_price
}));

await this.orderItemRepository.createManyWithTransaction(tx, orderItemsData);
  • Complete Example with Proper Flow
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
  return await this.withTransaction(async (tx) => {
    // 1. Validate business rules first
    await this.validateOrderData(orderData);
    
    // 2. Calculate total amount
    let totalAmount = 0;
    const validatedItems = [];
    
    for (const item of orderData.items) {
      const product = await this.productRepository.findByIdWithTransaction(tx, item.product_id);
      // ... validation logic
      totalAmount += product.price * item.quantity;
      validatedItems.push({
        product_id: item.product_id,
        quantity: item.quantity,
        unit_price: product.price
      });
    }

    // 3. Create the order (gets auto-generated ID)
    const newOrder = await this.orderRepository.createWithTransaction(tx, {
      user_id: orderData.user_id,
      total_amount: totalAmount,
      status: 'pending',
      created_at: new Date()
    });

    // 4. Create order items using the order ID
    for (const item of validatedItems) {
      await this.orderItemRepository.createWithTransaction(tx, {
        order_id: newOrder.id,  // 👈 Key: Use generated order ID
        product_id: item.product_id,
        quantity: item.quantity,
        unit_price: item.unit_price
      });
    }

    // 5. Update product inventory
    for (const item of orderData.items) {
      await this.productRepository.decrementStockWithTransaction(
        tx, 
        item.product_id, 
        item.quantity
      );
    }

    // 6. Return the complete order (with ID)
    return newOrder;
  });
}

🔧 Repository Method Requirements:

Your repositories need to support transaction-aware methods:

// In your OrderRepository
class OrderRepository extends BaseRepository<Order> {
  async createWithTransaction(tx: DatabaseTransaction, data: Partial<Order>): Promise<Order> {
    // Use tx.query() instead of this.db.query()
    const result = await tx.query(
      'INSERT INTO orders (user_id, total_amount, status, created_at) VALUES (?, ?, ?, ?) RETURNING *',
      [data.user_id, data.total_amount, data.status, data.created_at]
    );
    return result.rows[0];
  }
}

// In your OrderItemRepository  
class OrderItemRepository extends BaseRepository<OrderItem> {
  async createWithTransaction(tx: DatabaseTransaction, data: Partial<OrderItem>): Promise<OrderItem> {
    const result = await tx.query(
      'INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?) RETURNING *',
      [data.order_id, data.product_id, data.quantity, data.unit_price]
    );
    return result.rows[0];
  }

  async createManyWithTransaction(tx: DatabaseTransaction, items: Partial<OrderItem>[]): Promise<OrderItem[]> {
    // Bulk insert implementation
    const values = items.map(item => [item.order_id, item.product_id, item.quantity, item.unit_price]);
    // ... bulk insert logic
  }
}

💡 Key Points:

  • Order matters: Create parent record first to get the ID
  • Use the transaction: All operations must use the same tx parameter
  • ID is immediately available: After createWithTransaction, the returned object has the generated ID
  • All-or-nothing: If any step fails, the entire transaction rolls back
  • Performance: Consider bulk operations for multiple child records

3. Custom Validation Rules

export class ProductService extends BaseService {
  constructor(private productRepository: ProductRepository) {
    super();
    
    // Register custom validation rules
    this.validator.registerRule('sku', (value) => {
      const skuPattern = /^[A-Z]{2}-\d{4}$/;
      return skuPattern.test(value) ? null : 'SKU must follow format: XX-0000';
    });
  }

  async createProduct(productData: CreateProductRequest): Promise<Product> {
    const validationResult = await this.validate(productData, {
      name: [ValidationHelper.required(), ValidationHelper.minLength(2)],
      sku: [ValidationHelper.required(), this.validator.getRule('sku')!],
      price: [ValidationHelper.required(), ValidationHelper.numberRange(0.01, 10000)]
    });

    if (!validationResult.isValid) {
      throw new BusinessError('Invalid product data', 'VALIDATION_FAILED', undefined, {
        errors: validationResult.errors
      });
    }

    // Create product logic...
  }
}

API Reference

BaseService

The abstract base class that your services should extend.

Constructor Options

interface ServiceOptions {
  database?: DatabaseClient;     // For transaction support
  validator?: Validator;         // Custom validator instance
  config?: Record<string, any>;  // Service-specific config
}

Protected Methods

// Validation
protected async validate<T>(data: T, rules: ValidationRules): Promise<ValidationResult>
protected validateRequired(data: Record<string, any>, fields: string[]): void

// Transactions
protected async withTransaction<T>(callback: (tx: DatabaseTransaction) => Promise<T>): Promise<T>

// Error Handling
protected handleRepositoryError(error: Error): BusinessError

// Utilities
protected startOperation(operation: string, metadata?: Record<string, any>): () => void
protected safeSerialize(obj: any): any

🛡 BusinessError

Custom error class for business logic errors.

class BusinessError extends Error {
  constructor(
    message: string,
    code: string,
    originalError?: Error,
    context?: Record<string, any>
  )

  // Properties
  readonly code: string
  readonly originalError?: Error
  readonly context?: Record<string, any>
  readonly timestamp: Date

  // Methods
  toJSON(): Record<string, any>
  toUserResponse(): { error: string; code: string; timestamp: string }
  hasCode(code: string): boolean
  static isBusinessError(error: any): error is BusinessError
}

ValidationHelper

Pre-built validation rules for common scenarios.

class ValidationHelper {
  // Basic rules
  static required(): ValidationRule
  static minLength(min: number): ValidationRule
  static maxLength(max: number): ValidationRule
  static email(): ValidationRule
  static numberRange(min?: number, max?: number): ValidationRule
  static pattern(regex: RegExp, message: string): ValidationRule
  static oneOf(options: any[], message?: string): ValidationRule
  
  // Array rules
  static arrayMinLength(min: number): ValidationRule
  
  // Advanced rules
  static custom(validator: (value: any, data?: any) => string | null): ValidationRule
  static when(condition: (data: any) => boolean, rule: ValidationRule): ValidationRule
  
  // Utility methods
  static combine(...rules: ValidationRule[]): ValidationRule[]
  static entityId(): ValidationRule[]
  
  // Pre-configured rule sets
  static userCreation(): ValidationRules
  static pagination(): ValidationRules
}

✅ Validation System

Basic Validation

const result = await this.validate(data, {
  email: [ValidationHelper.required(), ValidationHelper.email()],
  age: [ValidationHelper.numberRange(18, 120)]
});

if (!result.isValid) {
  // Handle validation errors
  console.log(result.errors); // { email: ['Must be a valid email'], age: ['Must be at least 18'] }
}

🧠 Custom Validation Rules

// Register a custom rule
this.validator.registerRule('phone', (value) => {
  const phonePattern = /^\+?[\d\s-()]{10,}$/;
  return phonePattern.test(value) ? null : 'Invalid phone number format';
});

// Use in validation
const rules = {
  phone: [ValidationHelper.required(), this.validator.getRule('phone')!]
};

Conditional Validation

const rules = {
  email: [ValidationHelper.required(), ValidationHelper.email()],
  password: ValidationHelper.when(
    (data) => data.isNewUser === true,
    ValidationHelper.combine(
      ValidationHelper.required(),
      ValidationHelper.minLength(8)
    )
  )
};

Error Handling Patterns

1. Repository Error Transformation

The handleRepositoryError method automatically transforms common database errors:

// Database constraint violation → BusinessError with DUPLICATE_RESOURCE code
// Record not found → BusinessError with RESOURCE_NOT_FOUND code  
// Foreign key violation → BusinessError with CONSTRAINT_VIOLATION code
// Other errors → BusinessError with OPERATION_FAILED code

2. Structured Error Responses

try {
  const user = await userService.createUser(userData);
  return createSuccessResult(user);
} catch (error) {
  if (BusinessError.isBusinessError(error)) {
    // Handle business logic errors
    return createErrorResult(error);
  }
  // Handle unexpected errors
  throw error;
}

3. API Integration

export class UserController {
  async createUser(req: any, res: any) {
    try {
      const user = await this.userService.createUser(req.body);
      res.status(201).json(createSuccessResult(user));
    } catch (error) {
      const result = createErrorResult(error as Error);
      
      // Map business error codes to HTTP status codes
      let status = 500;
      if (BusinessError.isBusinessError(error)) {
        switch (error.code) {
          case 'VALIDATION_FAILED': status = 400; break;
          case 'USER_EXISTS': status = 409; break;
          case 'USER_NOT_FOUND': status = 404; break;
        }
      }
      
      res.status(status).json(result);
    }
  }
}

Transaction Management

Simple Transaction

async updateUserProfile(userId: number, profileData: any): Promise<User> {
  return await this.withTransaction(async (tx) => {
    // Update user
    const user = await this.userRepository.updateWithTransaction(tx, userId, {
      name: profileData.name,
      email: profileData.email
    });

    // Update user preferences
    await this.preferencesRepository.updateWithTransaction(tx, userId, {
      theme: profileData.theme,
      language: profileData.language
    });

    return user;
  });
}

Complex Business Transaction

async processOrder(orderData: CreateOrderRequest): Promise<Order> {
  return await this.withTransaction(async (tx) => {
    // 1. Validate inventory
    for (const item of orderData.items) {
      const product = await this.productRepository.findByIdWithTransaction(tx, item.productId);
      if (product.stock < item.quantity) {
        throw new BusinessError('Insufficient stock', 'INSUFFICIENT_STOCK');
      }
    }

    // 2. Create order
    const order = await this.orderRepository.createWithTransaction(tx, orderData);

    // 3. Update inventory
    for (const item of orderData.items) {
      await this.productRepository.decrementStockWithTransaction(
        tx, item.productId, item.quantity
      );
    }

    // 4. Send notification (example of non-transactional side effect)
    // Note: This should be handled outside the transaction
    // Consider using event-driven patterns for side effects

    return order;
  });
}

Best Practices

1. Keep Services Focused

// ✅ Good - Single responsibility
class UserService extends BaseService {
  async createUser(userData: CreateUserRequest): Promise<User> { }
  async updateUser(id: number, updates: UpdateUserRequest): Promise<User> { }
  async getUserById(id: number): Promise<User> { }
}

// ❌ Bad - Too many responsibilities
class UserService extends BaseService {
  async createUser(userData: CreateUserRequest): Promise<User> { }
  async sendWelcomeEmail(user: User): Promise<void> { } // Should be EmailService
  async generateReport(userId: number): Promise<Report> { } // Should be ReportService
}

2. Validate Input Early

// ✅ Good - Validate first
async createUser(userData: CreateUserRequest): Promise<User> {
  const validationResult = await this.validate(userData, this.getUserValidationRules());
  if (!validationResult.isValid) {
    throw new BusinessError('Validation failed', 'VALIDATION_FAILED', undefined, {
      errors: validationResult.errors
    });
  }
  
  // Continue with business logic...
}

// ❌ Bad - Validation mixed with business logic
async createUser(userData: CreateUserRequest): Promise<User> {
  const existingUser = await this.userRepository.findByEmail(userData.email);
  if (!userData.email) { // Too late for basic validation
    throw new Error('Email required');
  }
  // ...
}

3. Use Transactions for Multi-Repository Operations

// ✅ Good - Transaction for consistency
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
  await this.withTransaction(async (tx) => {
    await this.accountRepository.decrementBalanceWithTransaction(tx, fromAccount, amount);
    await this.accountRepository.incrementBalanceWithTransaction(tx, toAccount, amount);
    await this.transactionRepository.createWithTransaction(tx, {
      from_account: fromAccount,
      to_account: toAccount,
      amount
    });
  });
}

// ❌ Bad - No transaction, inconsistent state possible
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
  await this.accountRepository.decrementBalance(fromAccount, amount);
  await this.accountRepository.incrementBalance(toAccount, amount); // Could fail, leaving inconsistent state
  await this.transactionRepository.create({ from_account: fromAccount, to_account: toAccount, amount });
}

4. Handle Errors Appropriately

// ✅ Good - Structured error handling
async getUserById(id: number): Promise<User> {
  try {
    this.validateRequired({ id }, ['id']);
    
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new BusinessError('User not found', 'USER_NOT_FOUND', undefined, { userId: id });
    }
    
    return user;
  } catch (error) {
    if (BusinessError.isBusinessError(error)) {
      throw error; // Re-throw business errors
    }
    // Transform repository errors
    throw this.handleRepositoryError(error as Error);
  }
}

Integration with Other Packages

This service layer integrates seamlessly with other microframework packages:

import { createLogger } from '@groundbrick/logger';
import { DatabaseFactory } from '@groundbrick/db-postgres';
import { UserRepository } from '@groundbrick/repository-base';
import { UserService } from './UserService';

// Initialize database
const db = DatabaseFactory.getInstance({
  host: 'localhost',
  database: 'myapp',
  user: 'user',
  password: 'password'
});

// Initialize repository
const userRepository = new UserRepository(db);

// Initialize service with database for transaction support
const userService = new UserService(userRepository, { database: db });

// Use in application
const user = await userService.createUser({
  name: 'John Doe',
  email: 'john@example.com'
});

📜 License

MIT

Keywords

typescript

FAQs

Package last updated on 28 Oct 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts