@stratly/core
v0.0.8
Published
Clean architecture foundation for Stratly
Downloads
35
Readme
@stratly/core
Enterprise-grade Clean Architecture foundation for the Stratly AI-powered business management platform. This core package provides battle-tested abstractions, domain patterns, and architectural utilities that enable consistent, scalable, and maintainable development across different firebase codebases.
✨ What is @stratly/core?
@stratly/core is the foundational architecture package that powers Stratly's multi-tenant, multi-domain business platform. It implements Clean Architecture principles with Domain-Driven Design (DDD) patterns, specifically designed for enterprise-scale applications requiring strict tenant isolation, role-based access control, and event-driven capabilities.
🚀 Key Benefits
- 🏢 Multi-Tenant by Design - Built-in organizational isolation and secure tenant context propagation
- 🛡️ Enterprise Security - Role-based access control (RBAC) with granular permissions
- ⚡ Type-Safe - Complete TypeScript coverage with strict typing and validation
- 🔄 Event-Driven - Domain events with correlation IDs and audit trails
- 🧪 Testable - Clean architecture enables easy unit and integration testing
- 📈 Scalable - Proven patterns for complex business domains
🏗️ Architecture Overview
This package implements Clean Architecture principles with Domain-Driven Design (DDD) patterns, specifically designed for a multi-tenant, multi-domain business platform.
Core Principles
- Dependency Inversion: Inner layers don't depend on outer layers
- Multi-tenancy: Built-in organizational isolation (
orgId) - Domain-Driven Design: Rich domain models with business logic
- CQRS Ready: Separate command and query patterns
- Event-Driven: Domain events for loose coupling
- Type Safety: Full TypeScript support with strict typing
📦 Installation
Prerequisites
- Node.js 20.x or higher
- TypeScript 5.0 or higher
- npm 9.x or higher
Install via npm
# Install the core package
npm install @stratly/core
# Or with yarn
yarn add @stratly/core
# Or with pnpm
pnpm add @stratly/corePeer Dependencies
This package requires the following peer dependencies:
npm install firebase-admin firebase-functions🚀 Quick Start
Basic Example
Here's a complete example showing how to build a feature using clean architecture patterns:
import {
Entity,
ValueObject,
UseCase,
Result,
Repository,
UseCaseContext,
EntityProps,
RepositoryOptions,
DomainError,
ValidationError,
} from "@stratly/core";
// 1. Define a Value Object with validation
class Email extends ValueObject<string> {
protected validate(value: string): void {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new ValidationError("Invalid email format");
}
}
get domain(): string {
return this.value.split("@")[1];
}
}
// 2. Define an Entity with business logic
interface UserProps extends EntityProps {
email: Email;
name: string;
isActive: boolean;
}
class User extends Entity<UserProps> {
static create(
props: Omit<UserProps, keyof EntityProps>,
orgId: string,
): User {
return new User({
...props,
orgId,
createdAt: new Date(),
updatedAt: new Date(),
createdBy: "system",
updatedBy: "system",
});
}
get email(): Email {
return this._props.email;
}
get name(): string {
return this._props.name;
}
get isActive(): boolean {
return this._props.isActive;
}
activate(updatedBy: string): void {
this._props.isActive = true;
this.updateAudit(updatedBy);
}
deactivate(updatedBy: string): void {
this._props.isActive = false;
this.updateAudit(updatedBy);
}
}
// 3. Define Repository interface
interface UserRepository extends Repository<User> {
findByEmail(
email: Email,
options: RepositoryOptions,
): Promise<Result<User | null>>;
findActiveUsers(options: RepositoryOptions): Promise<Result<User[]>>;
}
// 4. Define Use Case with proper error handling
interface CreateUserRequest {
email: string;
name: string;
}
class CreateUserUseCase extends UseCase<CreateUserRequest, User> {
constructor(private userRepository: UserRepository) {
super();
}
async execute(
request: CreateUserRequest,
context: UseCaseContext,
): Promise<Result<User>> {
try {
// Validate input
const email = new Email(request.email);
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(email, {
orgId: context.tenant.orgId,
});
if (existingUser.isFailure) {
return Result.failure(existingUser.error);
}
if (existingUser.value) {
return Result.failure(
new DomainError("User with this email already exists", "USER_EXISTS"),
);
}
// Create new user
const user = User.create(
{
email,
name: request.name,
isActive: true,
},
context.tenant.orgId,
);
// Save to repository
const saveResult = await this.userRepository.save(user, {
orgId: context.tenant.orgId,
});
return saveResult;
} catch (error) {
return Result.failure(
new DomainError("Failed to create user", "CREATE_USER_ERROR", error),
);
}
}
}Usage in Firebase Functions
import { https } from "firebase-functions/v2";
import { FirebaseUserRepository } from "./infrastructure/repositories";
import { CreateUserUseCase } from "./application/usecases";
const userRepository = new FirebaseUserRepository();
const createUserUseCase = new CreateUserUseCase(userRepository);
export const createUser = https.onRequest(async (req, res) => {
try {
const context: UseCaseContext = {
tenant: {
orgId: req.headers["x-org-id"] as string,
userId: req.headers["x-user-id"] as string,
roles: ["ADMIN"],
permissions: ["USER_CREATE"],
},
};
const result = await createUserUseCase.execute(req.body, context);
result.match(
(user) => res.status(201).json(user.toJSON()),
(error) => res.status(400).json({ error: error.message }),
);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});Domain Patterns
Entities
Base class for domain entities with built-in audit fields and tenant isolation:
interface CustomerProps extends EntityProps {
name: string;
email: string;
}
class Customer extends Entity<CustomerProps> {
// Business logic methods
updateEmail(newEmail: string, updatedBy: string): void {
this._props = { ...this._props, email: newEmail };
this.updateAudit(updatedBy);
}
}Value Objects
Immutable objects that represent concepts with validation:
class Money extends ValueObject<{ amount: number; currency: string }> {
protected validate(value: { amount: number; currency: string }): void {
if (value.amount < 0) {
throw new ValidationError("Amount cannot be negative");
}
}
add(other: Money): Money {
// Implementation with currency validation
}
}Aggregate Roots
Entities that can raise domain events:
class Order extends AggregateRoot<OrderProps> {
placeOrder(): void {
// Business logic
this.addDomainEvent(
new OrderPlacedEvent({
aggregateId: this.id,
orgId: this.orgId,
// event data
}),
);
}
}Application Patterns
Use Cases
Orchestrate domain logic with cross-cutting concerns:
class CreateOrderUseCase extends UseCase<CreateOrderRequest, Order> {
async execute(
request: CreateOrderRequest,
context: UseCaseContext,
): Promise<Result<Order, DomainError>> {
// Validate permissions
const permissionCheck = this.validatePermissions(context, [
Permission.SALES_WRITE,
]);
if (permissionCheck.isFailure) {
return Result.failure(permissionCheck.error);
}
// Execute business logic
// ...
}
}Commands vs Queries
Separate read and write operations:
// Command (writes data)
class CreateCustomerCommand extends BaseCommand<
CreateCustomerRequest,
Customer
> {
async execute(request: CreateCustomerRequest, context: UseCaseContext) {
// Implementation
}
}
// Query (reads data)
class GetCustomersQuery extends PaginatedQuery<GetCustomersRequest, Customer> {
async execute(request: GetCustomersRequest, context: UseCaseContext) {
// Implementation with pagination
}
}Infrastructure Patterns
Repositories
Data access abstraction with tenant isolation:
class FirebaseCustomerRepository implements CustomerRepository {
async findById(
id: string,
options: RepositoryOptions,
): Promise<Result<Customer | null>> {
// Firebase implementation with orgId filtering
}
}Event Bus
Decouple domain events from handlers:
class OrderPlacedHandler extends BaseEventHandler<OrderPlacedEvent> {
async handle(event: OrderPlacedEvent): Promise<Result<void>> {
// Handle the event (send email, update inventory, etc.)
}
getEventName(): string {
return "OrderPlaced";
}
}Dependency Injection
Built-in DI container for managing dependencies:
import { DIContainer, Injectable } from "@stratly/core";
@Injectable("userRepository")
class FirebaseUserRepository implements UserRepository {
// Implementation
}
// Setup container
const container = new DIContainer();
container.register({
identifier: "userRepository",
implementation: FirebaseUserRepository,
singleton: true,
});
// Resolve dependencies
const repository = container.resolve<UserRepository>("userRepository");Multi-Tenant Security
Tenant Context
Every operation carries tenant information:
const context: UseCaseContext = {
tenant: {
orgId: "org-123",
userId: "user-456",
roles: [Role.MANAGER],
permissions: [Permission.SALES_READ, Permission.SALES_WRITE],
},
};Permission Validation
Built-in permission checking:
class SalesUseCase extends UseCase<any, any> {
async execute(request: any, context: UseCaseContext) {
// Automatic permission validation
const permissionCheck = this.validatePermissions(context, [
Permission.SALES_WRITE,
]);
if (permissionCheck.isFailure) {
return Result.failure(permissionCheck.error);
}
// ... rest of implementation
}
}Error Handling
Structured error handling with context:
try {
const result = await useCase.execute(request, context);
result.match(
(success) => {
// Handle success
},
(error) => {
// Handle error with full context
logger.error("Use case failed", error, {
orgId: context.tenant.orgId,
userId: context.tenant.userId,
});
},
);
} catch (error) {
// Handle unexpected errors
}Testing
The package provides utilities for testing domain logic:
describe("Customer Entity", () => {
it("should create customer with valid data", () => {
const customer = new Customer({
orgId: "org-123",
name: "John Doe",
email: "[email protected]",
});
expect(customer.name).toBe("John Doe");
expect(customer.email).toBe("[email protected]");
expect(customer.orgId).toBe("org-123");
});
});📋 API Reference
Core Exports
| Export | Type | Description |
| ------------------------------ | -------------- | ------------------------------------------------ |
| Entity<T> | Abstract Class | Base class for domain entities with audit fields |
| AggregateRoot<T> | Abstract Class | Entity that can raise domain events |
| ValueObject<T> | Abstract Class | Immutable value objects with validation |
| UseCase<TRequest, TResponse> | Abstract Class | Base class for application use cases |
| Repository<T> | Interface | Data access abstraction with tenant isolation |
| Result<T, E> | Class | Railway-oriented programming result type |
| DomainError | Class | Structured error handling for domain operations |
| UseCaseContext | Interface | Tenant and user context for operations |
Utility Functions
// Result helpers
Result.success<T>(value: T): Result<T>
Result.failure<E>(error: E): Result<never, E>
// Validation utilities
validateRequired(value: any, fieldName: string): void
validateEmail(email: string): void
validateId(id: string): void🧪 Testing
Running Tests
# Install dependencies
npm install
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test -- user.test.ts