typebus-cqrs
Version:
Simple, type-safe CQRS library with auto-registration and minimal boilerplate (typebus-cqrs)
757 lines (608 loc) โข 18.6 kB
Markdown
# TypeBus-CQRS
[TYPEBUS-CQRS]
|_|_|_|_|
o o
> Simple, type-safe CQRS library with auto-registration and minimal boilerplate
[](https://badge.fury.io/js/typebus-cqrs)
[](http://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
## ๐ฏ Why TypeBus-CQRS?
**Problem with existing CQRS libraries:**
- ๐คฏ Too much boilerplate code
- ๐ฆ Multiple imports for simple operations
- ๐จ Separate command and handler creation
- ๐ง Manual registration required
**TypeBus-CQRS solution:**
- โ
**One import** for everything
- โ
**Command + handler** in a single line
- โ
**Auto-registration** on creation
- โ
**Full type safety** with TypeScript
- โ
**Minimal boilerplate** - 3x less code
- โ
**Extensible types** - define your own commands/queries/events
### ๐ Ready for Use:
- โ
TypeScript 5.0+ support
- โ
Middleware pipeline
- โ
Built-in logging
- โ
Full test coverage
- โ
Production ready
- โ
Framework agnostic
- โ
Extensible type system
## ๐ Quick Start
### Installation
```bash
npm install typebus-cqrs reflect-metadata
# or
yarn add typebus-cqrs reflect-metadata
```
### Basic Usage
```typescript
import { createTypeBus, createCommand, createQuery } from 'typebus-cqrs';
// 1. Create TypeBus-CQRS instance
const bus = createTypeBus();
// 2. Create command with auto-registration (one line!)
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
// Your business logic here
console.log(`Creating user: ${data.name}`);
return { userId: aggregateId, events: ['User.Created'] };
}
);
// 3. Create query with auto-registration
const GetUser = createQuery(
bus,
'User.GetUser',
async (params) => {
// Your query logic here
return { id: params.userId, name: 'John Doe', email: 'john@example.com' };
}
);
// 4. Use them!
async function example() {
// Execute command
const result = await CreateUser.execute(
{ name: 'Alice', email: 'alice@example.com', password: 'secret' },
'user-123'
);
// Execute query
const user = await GetUser.execute({ userId: 'user-123' });
console.log('User created:', result);
console.log('User retrieved:', user);
}
```
That's it! **No separate classes, no manual registration, no complex setup.**
## ๐ง Extensible Type System
TypeBus-CQRS now supports **extensible types**, allowing you to define your own command, query, and event types without being limited by built-in types.
### Define Your Custom Types
```typescript
import { createTypeBus, createCommand, createQuery, ExtendCommandMap, ExtendQueryMap } from 'typebus-cqrs';
// Define your custom command types
interface CustomCommandMap {
'Product.CreateProduct': {
data: {
name: string;
price: number;
category: string;
};
aggregateId: string;
result: { productId: string; sku: string; events: string[] };
};
'Product.UpdatePrice': {
data: {
newPrice: number;
reason: string;
};
aggregateId: string;
result: { success: boolean; oldPrice: number; newPrice: number };
};
}
// Define your custom query types
interface CustomQueryMap {
'Product.GetProduct': {
params: { productId: string };
result: {
id: string;
name: string;
price: number;
category: string;
};
};
'Product.SearchProducts': {
params: {
searchTerm?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
};
result: {
products: Array<{ id: string; name: string; price: number }>;
total: number;
};
};
}
// Extend the built-in types with your custom types
type ExtendedCommandMap = ExtendCommandMap<CustomCommandMap>;
type ExtendedQueryMap = ExtendQueryMap<CustomQueryMap>;
// Create TypeBus with your extended types
const bus = createTypeBus<ExtendedCommandMap, ExtendedQueryMap>();
// Now you can use your custom types with full type safety!
const CreateProduct = createCommand(
bus,
'Product.CreateProduct', // โ TypeScript knows this type!
async (data, aggregateId) => {
// data is typed as { name: string; price: number; category: string; }
const sku = `SKU-${Date.now()}`;
return { productId: aggregateId, sku, events: ['Product.Created'] };
}
);
const GetProduct = createQuery(
bus,
'Product.GetProduct', // โ TypeScript knows this type!
async (params) => {
// params is typed as { productId: string }
return {
id: params.productId,
name: 'Sample Product',
price: 29.99,
category: 'Electronics'
};
}
);
// Usage with full type safety
await CreateProduct.execute(
{ name: 'Laptop', price: 999.99, category: 'Electronics' }, // โ Validated!
'prod-123'
);
const product = await GetProduct.execute({ productId: 'prod-123' }); // โ Validated!
```
### Alternative: Dynamic Types (Less Type Safety, More Flexibility)
If you prefer more flexibility over strict typing:
```typescript
const bus = createTypeBus(); // Uses 'any' types
const CreateOrder = createCommand(
bus,
'Order.CreateOrder' as any, // Use string literals
async (data: any, aggregateId: string) => {
return { orderId: aggregateId, status: 'pending' };
}
);
// No compile-time type checking, but maximum flexibility
await CreateOrder.execute({ userId: 'user-123', items: [] }, 'order-456');
```
## ๐ Documentation
### Core Concepts
#### Commands
Commands represent write operations that change state:
```typescript
const UpdateProfile = createCommand(
bus,
'User.UpdateProfile',
async (data, aggregateId, metadata) => {
// Validation
if (!data.name && !data.email) {
throw new Error('At least one field must be updated');
}
// Business logic
await updateUserInDatabase(aggregateId, data);
// Publish event
await UserProfileUpdated.publish(
{ changes: data },
aggregateId,
2 // version
);
return { success: true };
}
);
// Usage
await UpdateProfile.execute(
{ name: 'New Name', email: 'new@email.com' },
'user-123',
{ updatedBy: 'admin' } // metadata
);
```
#### Queries
Queries represent read operations:
```typescript
const SearchUsers = createQuery(
bus,
'User.SearchUsers',
async (params) => {
const { searchTerm, page = 1, limit = 10 } = params;
// Query logic
const users = await searchInDatabase(searchTerm, page, limit);
return {
users,
total: users.length,
page,
hasMore: users.length === limit
};
}
);
// Usage
const results = await SearchUsers.execute({
searchTerm: 'john',
page: 1,
limit: 20
});
```
#### Events
Events represent things that have happened:
```typescript
const UserCreated = createEventHandler(
bus,
'User.Created',
async (data, aggregateId, version, metadata) => {
// Update read model
await updateUserReadModel(aggregateId, data);
// Send welcome email
await sendWelcomeEmail(data.email, data.name);
// Track analytics
await analytics.track('user_created', {
userId: aggregateId,
source: metadata?.source
});
}
);
// Publishing events
await UserCreated.publish(
{ name: 'John', email: 'john@example.com' },
'user-123',
1
);
```
### Alternative Usage Patterns
#### 1. Direct Bus Usage
```typescript
// Type-safe direct execution
const result = await bus.executeCommand(
'User.CreateUser', // โ Auto-completion!
{ name: 'John', email: 'john@example.com', password: 'secret' },
'user-123'
);
const user = await bus.executeQuery(
'User.GetUser', // โ Type-checked!
{ userId: 'user-123' }
);
```
#### 2. Fluent API
```typescript
import { fluent } from 'typebus-cqrs';
const userCommands = fluent(bus)
.command('User.CreateUser')
.handle(async (data, aggregateId) => {
// Handler logic
return { userId: aggregateId, events: [] };
});
const userQueries = fluent(bus)
.query('User.GetUser')
.handle(async (params) => {
// Query logic
return { id: params.userId, name: 'John' };
});
```
#### 3. Batch Registration
```typescript
const userHandlers = fluent(bus)
.batch()
.addCommand('createUser', 'User.CreateUser', async (data, id) => {
return { userId: id, events: [] };
})
.addQuery('getUser', 'User.GetUser', async (params) => {
return { id: params.userId, name: 'John' };
})
.build();
// Usage
await userHandlers.createUser.execute(data, 'user-123');
const user = await userHandlers.getUser.execute({ userId: 'user-123' });
```
### Middleware
TypeBus-CQRS supports middleware for cross-cutting concerns:
```typescript
import { withLogging } from 'typebus-cqrs';
const bus = createTypeBus();
// Add logging middleware
bus.use(withLogging({
level: 'info',
includeData: true
}));
// Custom middleware
bus.use({
async execute(message, next) {
console.log(`Executing: ${message.type}`);
try {
const result = await next(message);
console.log(`Success: ${message.type}`);
return result;
} catch (error) {
console.error(`Error in ${message.type}:`, error.message);
throw error;
}
}
});
```
### Type Safety
TypeBus-CQRS provides full type safety through TypeScript:
```typescript
// 1. Define your message types
interface CommandMap {
'User.CreateUser': {
data: { name: string; email: string; password: string };
aggregateId: string;
result: { userId: string; events: string[] };
};
}
// 2. TypeScript automatically infers and validates everything
const CreateUser = createCommand(
bus,
'User.CreateUser', // โ Must match CommandMap key
async (data, aggregateId) => {
// data is automatically typed as { name: string; email: string; password: string }
// aggregateId is automatically typed as string
// Return type must match CommandMap['User.CreateUser']['result']
return { userId: aggregateId, events: ['User.Created'] };
}
);
// 3. Usage is also type-safe
await CreateUser.execute(
{ name: 'John', email: 'john@test.com', password: 'secret' }, // โ Validated!
'user-123'
);
```
## ๐งช Testing
TypeBus-CQRS is designed to be easily testable:
```typescript
describe('User Commands', () => {
let bus: TypeBus;
beforeEach(() => {
bus = new TypeBus({ enableLogging: false }); // Clean instance per test
});
afterEach(() => {
bus.clear(); // Clean up
});
it('should create user', async () => {
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
return { userId: aggregateId, events: ['User.Created'] };
}
);
const result = await CreateUser.execute(
{ name: 'Test User', email: 'test@example.com', password: 'secret' },
'test-user-1'
);
expect(result.userId).toBe('test-user-1');
expect(result.events).toContain('User.Created');
});
});
```
## ๐ Framework Integration
### Express.js
```typescript
import express from 'express';
import { createTypeBus, createCommand } from 'typebus-cqrs';
const app = express();
const bus = createTypeBus();
// Register commands
const CreateUser = createCommand(bus, 'User.CreateUser', async (data, id) => {
return { userId: id, events: [] };
});
// Use in routes
app.post('/users', async (req, res) => {
try {
const result = await CreateUser.execute(req.body, `user-${Date.now()}`);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
```
### NestJS
```typescript
import { Injectable } from '@nestjs/common';
import { createTypeBus } from 'typebus-cqrs';
@Injectable()
export class UserService {
private bus = createTypeBus();
constructor() {
this.setupCommands();
}
private setupCommands() {
this.createUser = createCommand(
this.bus,
'User.CreateUser',
async (data, aggregateId) => {
// Business logic
return { userId: aggregateId, events: [] };
}
);
}
}
```
## ๐ Comparison with Other Libraries
| Feature | TypeBus-CQRS | NestJS CQRS | Wolkenkit | EventStore |
|---------|-------------|-------------|-----------|------------|
| **Simplicity** | โ
10/10 | โ 6/10 | โ 4/10 | โ 5/10 |
| **Minimal Code** | โ
10/10 | โ 4/10 | โ 3/10 | โ 4/10 |
| **Type Safety** | โ
9/10 | โ
9/10 | โ ๏ธ 7/10 | โ ๏ธ 7/10 |
| **Auto Registration** | โ
10/10 | โ 5/10 | โ 4/10 | โ 4/10 |
| **Learning Curve** | โ
Easy | โ ๏ธ Medium | โ Hard | โ ๏ธ Medium |
| **Extensible Types** | โ
10/10 | โ 3/10 | โ 2/10 | โ 3/10 |
### Before (NestJS CQRS):
```typescript
// 1. Create command class
export class CreateUserCommand {
constructor(public readonly name: string, public readonly email: string) {}
}
// 2. Create handler class
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
async execute(command: CreateUserCommand): Promise<void> {
// Logic here
}
}
// 3. Register in module
@Module({
providers: [CreateUserHandler],
})
export class UserModule {}
// 4. Use with imports
import { CreateUserCommand } from './commands/CreateUserCommand';
const command = new CreateUserCommand('John', 'john@example.com');
await this.commandBus.execute(command);
```
### After (TypeBus-CQRS):
```typescript
// 1. One line creates command + handler + registration
const CreateUser = createCommand(bus, 'User.CreateUser', async (data, id) => {
// Logic here
});
// 2. Use directly - no imports needed
await CreateUser.execute({ name: 'John', email: 'john@example.com' }, 'user-123');
```
**TypeBus-CQRS** - Because CQRS should be simple, not complicated. ๐โจ
## ๐ ๏ธ Advanced Features
### Custom Middleware
```typescript
// Validation middleware
const validationMiddleware = {
async execute(message, next) {
// Validate message based on type
if (message.type === 'User.CreateUser') {
const { data } = message as any;
if (!data.email?.includes('@')) {
throw new Error('Invalid email');
}
}
return await next(message);
}
};
bus.use(validationMiddleware);
```
### Metrics and Monitoring
```typescript
const bus = createTypeBus({
enableLogging: true,
enableMetrics: true
});
// Get stats
console.log(bus.getStats());
// {
// commandHandlers: 5,
// queryHandlers: 3,
// eventHandlers: 8,
// middleware: 2
// }
```
### Error Handling
```typescript
const CreateUser = createCommand(
bus,
'User.CreateUser',
async (data, aggregateId) => {
try {
// Business logic
const user = await userService.create(data);
// Publish success event
await UserCreated.publish(user, aggregateId, 1);
return { userId: user.id, events: ['User.Created'] };
} catch (error) {
// Publish failure event
await UserCreationFailed.publish(
{ reason: error.message, data },
aggregateId,
1
);
throw error;
}
}
);
```
## ๐ License
MIT License
Copyright (c) 2024 Bayramov TG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## ๐ค Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
## ๐ฎ Roadmap
- [ ] MongoDB adapter for event store
- [ ] Redis adapter for caching
- [ ] Saga pattern support
- [ ] GraphQL integration
- [ ] OpenTelemetry integration
- [ ] VS Code extension for auto-completion
## โก Setup & Usage
### 1. Install Dependencies
```bash
npm install
# or
yarn install
```
### 2. Run Examples
#### Basic Example
```bash
npm run example:basic
# or
npm run dev
```
#### Advanced Example
```bash
npm run example:advanced
```
#### Extensible Types Example
```bash
npm run example:extensible
```
### 3. Development
```bash
# Build the project
npm run build
# Build with watch mode
npm run build:watch
# Run tests
npm test
# Watch tests
npm run test:watch
# Check test coverage
npm run test:coverage
# Linting
npm run lint
# Auto-fix lint issues
npm run lint:fix
# Code formatting
npm run format
```
### ๐งช Quick Test
After installing dependencies, run:
```bash
npm run example:basic
```
You should see something like:
```
๐ TypeBus-CQRS Basic Usage Example
๐ฆ Creating commands, queries, and event handlers...
โถ๏ธ Executing commands and queries...
1. Creating user...
๐ค SUCCESS: User.CreateUser { id: 'cmd-...', duration: '102.34ms' }
โ
Result: { userId: 'user-001', events: ['User.Created'] }
2. Getting user...
๐ฅ SUCCESS: User.GetUser { id: 'qry-...', duration: '51.23ms' }
โ
User: { id: 'user-001', name: 'John Doe', email: 'john@example.com' }
โ
Basic example completed!
```