JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 112
  • Score
    100M100P100Q55109F
  • License MIT

Uma implementação robusta do Result Pattern para TypeScript com wrappers para integração legada

Package Exports

  • usecase_ts
  • usecase_ts/lib/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (usecase_ts) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

CI npm version License: MIT Coverage

usecase_ts

A robust Result Pattern implementation for TypeScript, designed to manage error flows elegantly and predictably. Inspired by u-case and optimized for modern development.

🎯 Why use usecase_ts?

  • Zero Exceptions: Eliminate unnecessary try/catch and unhandled errors
  • Type Safety: Complete typing with TypeScript generics
  • Fluent API: Elegant operation chaining with and_then
  • Legacy Integration: Transform any function/value into Result
  • Framework Agnostic: Works with any framework (NestJS, Express, etc.)
  • Rich Error Handling: Custom error type mapping
  • Context Tracking: Automatic context tracking

📦 Instalação

npm install usecase_ts

🎮 Exemplos Práticos

Execute exemplos completos para ver todas as funcionalidades:

# Demo completo com todas as funcionalidades
npm run demo

# Foco em value wrapping
npm run value-wrap

# Showcase completo
npm run showcase

# Menu interativo de exemplos
npm run examples

🚀 Quick Start

import { UseCase, Success, Failure, ResultWrapValue } from 'usecase_ts';

// Simple Use Case
class GetUserUseCase extends UseCase<{ id: string }, { name: string, email: string }> {
  async execute(input: { id: string }) {
    if (!input.id) {
      return Failure(new Error('ID is required'), 'VALIDATION_ERROR');
    }
    
    return Success({ name: 'John Doe', email: 'john@example.com' });
  }
}

// Basic usage
const result = await GetUserUseCase.call({ id: '123' });

result
  .onSuccess((user) => console.log('User:', user))
  .onFailure((error) => console.error('Error:', error.message));

// Transform existing values into Results
const existingValue = "Hello World";
const wrappedResult = ResultWrapValue(existingValue);
// → Success<string>

const errorValue = new Error("Something went wrong");
const wrappedError = ResultWrapValue(errorValue);
// → Failure<any>

🎨 Core Concepts

1. Result Pattern

Every operation returns a Result<T> that can be:

// Success - contains data
Success(data)

// Failure - contains error and type
Failure(error, type)

2. Use Cases

Encapsulate business logic in classes that extend UseCase<Input, Output>:

class CalculateUseCase extends UseCase<{ a: number, b: number }, { result: number }> {
  async execute(input: { a: number, b: number }) {
    if (typeof input.a !== 'number' || typeof input.b !== 'number') {
      return Failure(new Error('Invalid input'), 'VALIDATION_ERROR');
    }
    
    return Success({ result: input.a + input.b });
  }
}

3. Wrappers

3.1 ResultWrapper - For Functions

Transform any function into one that returns Result:

import { ResultWrapper, ValidationError } from 'usecase_ts';

// Existing function that might throw
const validateEmail = (email: string) => {
  if (!email.includes('@')) throw new ValidationError('Invalid email');
  return true;
};

// Wrapped - will never throw exceptions
const result = ResultWrapper(validateEmail, ['email@test.com'], {
  errorMappings: [
    { errorType: ValidationError, failureType: 'VALIDATION_ERROR' }
  ]
});

result
  .onSuccess(() => console.log('Valid email!'))
  .onFailure((error) => console.log('Invalid email:', error.message), 'VALIDATION_ERROR');

3.2 ResultWrapValue - For Executed Values

Transform already executed values (including errors, null, undefined) into Results:

import { ResultWrapValue } from 'usecase_ts';

// Scenario 1: Valid value
const data = { id: 1, name: 'John' };
const result1 = ResultWrapValue(data);
// → Success<{id: number, name: string}>

// Scenario 2: Caught error
let capturedError: Error | null = null;
try {
  JSON.parse('invalid json');
} catch (error) {
  capturedError = error as Error;
}
const result2 = ResultWrapValue(capturedError);
// → Failure<any>

// Scenario 3: Value that might be null/undefined
const user = findUserById('999'); // might return null
const result3 = ResultWrapValue(user, {
  nullAsFailure: true,
  defaultFailureType: 'USER_NOT_FOUND'
});
// → Failure if user is null

// Scenario 4: Custom validations
const result4 = ResultWrapValue(someValue, {
  customValidation: (value) => {
    if (value < 0) return 'Value must be positive';
    return true;
  }
});

🔧 Advanced Features

1. Error Mapping

import { 
  ValidationError, 
  AuthenticationError, 
  NotFoundError,
  ConflictError,
  AuthorizationError 
} from 'usecase_ts';

const errorMappings = [
  { errorType: ValidationError, failureType: 'VALIDATION_ERROR' },
  { errorType: AuthenticationError, failureType: 'AUTH_ERROR' },
  { errorType: NotFoundError, failureType: 'NOT_FOUND' },
  { errorType: ConflictError, failureType: 'CONFLICT' },
  { errorType: AuthorizationError, failureType: 'FORBIDDEN' }
];

// Use in any wrapper
const result = ResultWrapper(riskyFunction, [params], { errorMappings });

2. Operation Chaining

const result = await ValidateInputUseCase.call({ email: 'user@test.com' })
  .and_then(async (data) => FindUserUseCase.call({ email: data.email }))
  .and_then(async (user) => SendEmailUseCase.call({ userId: user.id }))
  .and_then(async (emailResult) => LogActivityUseCase.call({ 
    action: 'email_sent', 
    success: emailResult.sent 
  }));

result
  .onSuccess((log) => console.log('Complete process:', log))
  .onFailure((error) => console.error('Validation failed'), 'VALIDATION_ERROR')
  .onFailure((error) => console.error('User not found'), 'NOT_FOUND')
  .onFailure((error) => console.error('General failure'));

3. Advanced Validations with ResultWrapValue

// Example: API response validation
const apiResponse = await fetch('/api/user/123').then(r => r.json());

const validatedResponse = ResultWrapValue(apiResponse, {
  // Basic validations
  nullAsFailure: true,
  undefinedAsFailure: true,
  emptyObjectAsFailure: true,
  
  // Custom validation
  customValidation: (user) => {
    if (!user.id) return 'ID is required';
    if (!user.email?.includes('@')) return 'Invalid email';
    if (!user.name || user.name.length < 2) return 'Name too short';
    return true;
  },
  
  // Context for debugging
  context: { source: 'api_user_fetch' },
  useCaseClass: 'UserValidation'
});

validatedResponse
  .onSuccess((user) => console.log('Valid user:', user))
  .onFailure((error) => console.error('Invalid user:', error.message));

4. Async/Await Integration

import { ResultWrapValueAsync } from 'usecase_ts';

// For Promises or async values
const processUser = async (userId: string) => {
  const userPromise = fetch(`/api/users/${userId}`).then(r => r.json());
  
  const result = await ResultWrapValueAsync(userPromise, {
    customValidation: (user) => {
      if (!user || !user.active) return 'Inactive user';
      return true;
    },
    errorMappings: [
      { errorType: Error, failureType: 'API_ERROR' }
    ]
  });
  
  return result;
};

🏗️ Real-World Examples

1. Service Layer with Error Handling

class UserService {
  async fetchUser(id: string): Promise<User | null> {
    try {
      const response = await fetch(`/api/users/${id}`);
      if (response.status === 404) return null;
      if (response.status === 401) throw new AuthenticationError('Token expired');
      if (!response.ok) throw new Error('API error');
      return response.json();
    } catch (error) {
      throw error;
    }
  }

  validateUser(user: User): boolean {
    if (!user.email) throw new ValidationError('Email required');
    if (!user.name) throw new ValidationError('Name required');
    return true;
  }
}

class GetValidatedUserUseCase extends UseCase<{ id: string }, User> {
  constructor(private userService: UserService) {
    super();
  }

  async execute(input: { id: string }) {
    const errorMappings = [
      { errorType: ValidationError, failureType: 'VALIDATION_ERROR' },
      { errorType: AuthenticationError, failureType: 'AUTH_ERROR' },
      { errorType: NotFoundError, failureType: 'NOT_FOUND' }
    ];

    // 1. Fetch user (might return null)
    const user = await this.userService.fetchUser(input.id);
    
    // 2. Validate existence using ResultWrapValue
    const userExistsResult = ResultWrapValue(user, {
      nullAsFailure: true,
      defaultFailureType: 'NOT_FOUND'
    });
    
    if (userExistsResult.isFailure()) {
      return Failure(new Error('User not found'), 'NOT_FOUND');
    }

    // 3. Validate user data using ResultWrapper
    const validationResult = ResultWrapper(
      this.userService.validateUser.bind(this.userService),
      [user],
      { errorMappings }
    );

    if (validationResult.isFailure()) {
      return Failure(validationResult.getError(), validationResult.getType());
    }

    return Success(user);
  }
}

// Usage
const result = await GetValidatedUserUseCase.call({ id: '123' });

result
  .onSuccess((user) => console.log('Valid user:', user))
  .onFailure((error) => console.error('Validation failed'), 'VALIDATION_ERROR')
  .onFailure((error) => console.error('User not found'), 'NOT_FOUND')
  .onFailure((error) => console.error('Invalid token'), 'AUTH_ERROR');

2. Complete NestJS Integration

import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { UseCase, Success, Failure, ResultWrapValue, ValidationError } from 'usecase_ts';

interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

interface CreateUserOutput {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

@Injectable()
export class CreateUserUseCase extends UseCase<CreateUserInput, CreateUserOutput> {
  constructor(
    private userRepository: UserRepository,
    private passwordService: PasswordService,
    private emailService: EmailService
  ) {
    super();
  }

  async execute(input: CreateUserInput) {
    // 1. Validar input usando ResultWrapValue
    const inputValidation = ResultWrapValue(input, {
      customValidation: (data) => {
        if (!data.name || data.name.length < 2) return 'Nome deve ter pelo menos 2 caracteres';
        if (!data.email?.includes('@')) return 'Email inválido';
        if (!data.password || data.password.length < 8) return 'Senha deve ter pelo menos 8 caracteres';
        return true;
      },
      defaultFailureType: 'VALIDATION_ERROR'
    });

    if (inputValidation.isFailure()) {
      return Failure(inputValidation.getError(), inputValidation.getType());
    }

    // 2. Check if email already exists
    const existingUser = await this.userRepository.findByEmail(input.email);
    const emailCheck = ResultWrapValue(existingUser, {
      customValidation: (user) => {
        if (user !== null) return 'Email já está em uso';
        return true;
      },
      defaultFailureType: 'CONFLICT_ERROR'
    });

    if (emailCheck.isFailure()) {
      return Failure(emailCheck.getError(), emailCheck.getType());
    }

    // 3. Hash da senha usando ResultAsyncWrapper
    const hashResult = await ResultAsyncWrapper(
      this.passwordService.hash.bind(this.passwordService),
      [input.password],
      { defaultFailureType: 'HASH_ERROR' }
    );

    if (hashResult.isFailure()) {
      return Failure(hashResult.getError(), hashResult.getType());
    }

    // 4. Create user
    const createResult = await ResultAsyncWrapper(
      this.userRepository.create.bind(this.userRepository),
      [{
        name: input.name,
        email: input.email,
        passwordHash: hashResult.getValue()
      }],
      { defaultFailureType: 'DATABASE_ERROR' }
    );

    if (createResult.isFailure()) {
      return Failure(createResult.getError(), createResult.getType());
    }

    // 5. Enviar email de boas-vindas (não-blocking)
    ResultAsyncWrapper(
      this.emailService.sendWelcome.bind(this.emailService),
      [input.email, input.name]
    ).then(emailResult => {
      if (emailResult.isFailure()) {
        console.warn('Falha ao enviar email:', emailResult.getError().message);
      }
    });

    return Success(createResult.getValue());
  }
}

// Controller
@Controller('users')
export class UserController {
  constructor(private createUserUseCase: CreateUserUseCase) {}

  @Post()
  async createUser(@Body() body: CreateUserInput) {
    const result = await this.createUserUseCase.call(body);
    
    return result
      .onSuccess((user) => ({ success: true, data: user }))
      .onFailure((error) => {
        throw new BadRequestException(error.message);
      }, 'VALIDATION_ERROR')
      .onFailure((error) => {
        throw new ConflictException(error.message);
      }, 'CONFLICT_ERROR')
      .onFailure((error) => {
        throw new InternalServerErrorException('Erro interno');
      });
  }
}

3. Data Processing Pipeline

// Data processing pipeline with robust error handling
class DataProcessingPipeline {
  async processCSVFile(file: File) {
    return ProcessFileUseCase.call({ file })
      .and_then(async (data) => {
        // Validar cada linha do CSV
        const validatedRows = data.rows.map(row => 
          ResultWrapValue(row, {
            customValidation: (row) => {
              if (!row.email?.includes('@')) return `Linha ${row.line}: Email inválido`;
              if (!row.name) return `Line ${row.line}: Name required`;
              return true;
            }
          })
        );

        const errors = validatedRows.filter(r => r.isFailure());
        if (errors.length > 0) {
          return Failure(
            new Error(`${errors.length} linhas inválidas`),
            'VALIDATION_ERROR'
          );
        }

        return Success({ validRows: validatedRows.map(r => r.getValue()) });
      })
      .and_then(async (data) => SaveDataUseCase.call({ rows: data.validRows }))
      .and_then(async (result) => SendNotificationUseCase.call({ 
        message: `${result.saved} registros processados` 
      }));
  }
}

📊 Comparison: Before vs After

❌ Before (with traditional try/catch)

class UserService {
  async getUser(id: string) {
    try {
      const user = await this.repository.findById(id);
      if (!user) {
        throw new Error('User not found');
      }
      
      if (!user.email?.includes('@')) {
        throw new Error('Invalid email');
      }
      
      return user;
    } catch (error) {
      // Error can be anything
      console.error(error);
      throw error; // Propagates error
    }
  }
}

// Usage - always need try/catch
try {
  const user = await userService.getUser('123');
  console.log(user);
} catch (error) {
  // Don't know what type of error it is
  console.error(error);
}

✅ After (with usecase_ts)

class GetUserUseCase extends UseCase<{ id: string }, User> {
  async execute(input: { id: string }) {
    const user = await this.repository.findById(input.id);
    
    // Use ResultWrapValue to validate
    return ResultWrapValue(user, {
      nullAsFailure: true,
      customValidation: (u) => {
        if (!u.email?.includes('@')) return 'Invalid email';
        return true;
      },
      defaultFailureType: 'USER_NOT_FOUND'
    });
  }
}

// Usage - no try/catch, typed error handling
const result = await GetUserUseCase.call({ id: '123' });

result
  .onSuccess((user) => console.log('User:', user))
  .onFailure((error) => console.error('User not found'), 'USER_NOT_FOUND')
  .onFailure((error) => console.error('Validation error'), 'VALIDATION_ERROR');

📚 Complete API Reference

Core Classes

Result<T>

interface Result<T> {
  getValue(): T;                          // Get success value
  getError(): Error;                      // Get error
  getType(): string;                      // Get type ('SUCCESS', 'FAILURE', custom)
  isSuccess(): boolean;                   // Check if success
  isFailure(): boolean;                   // Check if failure
  and_then<U>(fn): Promise<Result<U>>;    // Chain operations
  onSuccess(fn): Result<T>;               // Success callback
  onFailure(fn, type?): Result<T>;        // Failure callback
  context?: Record<string, any>;          // Optional context
  useCaseClass?: string;                  // Nome da classe do use case
}

UseCase<I, O>

abstract class UseCase<I, O> {
  abstract execute(input: I): Promise<Result<O>>;
  call(input: I): ResultPromise<O>;
  static call<I, O>(input: I): ResultPromise<O>;
}

Factory Functions

Success<T>(value, context?, useCaseClass?): Result<T>

Criar um resultado de sucesso.

Failure<T>(error, type?, context?, useCaseClass?): Result<T>

Criar um resultado de falha.

Wrapper Functions

ResultWrapper<T>(fn, params?, options?): Result<T>

Wrap synchronous functions to return Results.

// Without parameters
const result1 = ResultWrapper(() => getCurrentTime());

// With parameters
const result2 = ResultWrapper(addNumbers, [5, 3]);

// With options
const result3 = ResultWrapper(validateEmail, ['test@example.com'], {
  errorMappings: [{ errorType: ValidationError, failureType: 'VALIDATION_ERROR' }]
});

ResultAsyncWrapper<T>(fn, params?, options?): Promise<Result<T>>

Wrap asynchronous functions to return Results.

ResultWrapValue<T>(value, options?): Result<T>

Wrap already executed values into Results.

// Valor simples
const result1 = ResultWrapValue("hello");

// Com validações
const result2 = ResultWrapValue(user, {
  nullAsFailure: true,
  customValidation: (u) => u.email ? true : 'Email required'
});

// Erro capturado
const result3 = ResultWrapValue(caughtError);

ResultWrapValueAsync<T>(value, options?): Promise<Result<T>>

Wrap async values/Promises into Results.

// Promise
const result1 = await ResultWrapValueAsync(fetchUser());

// Value with async validation
const result2 = await ResultWrapValueAsync(someValue, {
  customValidation: async (val) => await validateWithAPI(val)
});

Error Classes Pré-definidas

ValidationError     // For validation errors
AuthenticationError // Para erros de autenticação  
AuthorizationError  // Para erros de autorização
NotFoundError      // Para recursos não encontrados
ConflictError      // Para conflitos de dados

Configuration Types

interface WrapperOptions {
  errorMappings?: Array<{
    errorType: new (...args: any[]) => Error;
    failureType: string;
  }>;
  defaultFailureType?: string;
  context?: Record<string, any>;
  useCaseClass?: string;
}

interface ValueWrapperOptions extends WrapperOptions {
  nullAsFailure?: boolean;           // null → Failure
  undefinedAsFailure?: boolean;      // undefined → Failure  
  emptyStringAsFailure?: boolean;    // "" → Failure
  zeroAsFailure?: boolean;           // 0 → Failure
  emptyArrayAsFailure?: boolean;     // [] → Failure
  emptyObjectAsFailure?: boolean;    // {} → Failure
  customValidation?: (value: any) => boolean | string;
}

🎯 Best Practices

1. Always return Results

// ❌ Don't do
async execute(input) {
  if (!input.valid) throw new Error('Invalid');
  return data;
}

// ✅ Do
async execute(input) {
  if (!input.valid) return Failure(new Error('Invalid'), 'VALIDATION_ERROR');
  return Success(data);
}

2. Use wrappers for legacy code

// ❌ Don't change existing functions
const user = await legacyFetchUser(id); // might throw exception

// ✅ Wrapper for safety
const result = await ResultAsyncWrapper(legacyFetchUser, [id], {
  errorMappings: [{ errorType: NotFoundError, failureType: 'NOT_FOUND' }]
});

3. Use ResultWrapValue for already processed values

// ❌ Manual checks
if (user === null) {
  throw new Error('User not found');
}
if (!user.email) {
  throw new Error('Email required');
}

// ✅ Validation with ResultWrapValue
const result = ResultWrapValue(user, {
  nullAsFailure: true,
  customValidation: (u) => u.email ? true : 'Email required',
  defaultFailureType: 'USER_INVALID'
});

4. Chain operations

// ✅ Fluent chaining
const result = await FirstUseCase.call(input)
  .and_then(async (data) => SecondUseCase.call(data))
  .and_then(async (data) => ThirdUseCase.call(data));

5. Handle different error types

result
  .onSuccess((data) => handleSuccess(data))
  .onFailure((error) => handleValidation(error), 'VALIDATION_ERROR')
  .onFailure((error) => handleNotFound(error), 'NOT_FOUND')
  .onFailure((error) => handleGeneric(error)); // Catch-all

🚀 Features

  • Type Safety: Complete TypeScript support with generics
  • Zero Dependencies: No external dependencies
  • Fluent API: Chainable operations with and_then
  • Error Mapping: Transform any error into typed failures
  • Context Tracking: Automatic context preservation
  • Legacy Integration: Wrap existing functions with wrappers
  • Value Wrapping: Transform values/errors into Results
  • Framework Agnostic: Works with any framework
  • NestJS Ready: Perfect integration with dependency injection
  • Rich Examples: Complete examples in /examples folder
  • Interactive Demos: Run npm run demo for hands-on experience

🔄 Migration Guide

From Exception-based to Result-based

// Before
class OldService {
  async getUser(id: string): Promise<User> {
    const user = await this.db.findUser(id);
    if (!user) throw new Error('Not found');
    if (!user.active) throw new Error('Inactive');
    return user;
  }
}

// After  
class NewService extends UseCase<{id: string}, User> {
  async execute(input: {id: string}) {
    const user = await this.db.findUser(input.id);
    
    return ResultWrapValue(user, {
      nullAsFailure: true,
      customValidation: (u) => u.active ? true : 'Inactive user',
      defaultFailureType: 'USER_NOT_FOUND'
    });
  }
}

📄 License

MIT - see the LICENSE file for details.

🤝 Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/new-feature)
  3. Write tests for your changes
  4. Make sure all tests pass (npm test)
  5. Commit your changes (git commit -m 'Add new feature')
  6. Push to the branch (git push origin feature/new-feature)
  7. Open a Pull Request

📊 Stats

  • 97 tests passing
  • 93% coverage
  • Zero dependencies
  • TypeScript first
  • Production ready

Built with ❤️ for the TypeScript community