emkore 0.3.3
emkore: ^0.3.3 copied to clipboard
Clean Architecture core library for Dart with use cases, entities, validation, authorization, and interceptor pipelines.
emkore #
A foundational Clean Architecture core library for Dart that provides abstract base classes, interfaces, and utilities for building scalable business applications.
🎯 What is emkore? #
Emkore is a production-grade Clean Architecture foundation that eliminates boilerplate and provides battle-tested patterns for:
- Use Cases & Interactors with built-in validation and authorization
- Entity Management with system fields and type safety
- Permission-Based Authorization with fine-grained access control
- Interceptor Pipeline for cross-cutting concerns (logging, metrics, auditing)
- Repository Patterns with generic interfaces
- Validation Framework with fluent API and JSON schema generation
🏗️ Project Structure & Conventions #
Recommended Project Structure #
Following Clean Architecture principles, organize your project like this:
your_project/
├── lib/
│ ├── src/
│ │ ├── entity/ # Domain entities and DTOs
│ │ │ └── user/
│ │ │ ├── user.entity.dart # Domain entity
│ │ │ └── user.dto.dart # Data transfer object
│ │ ├── usecase/ # Use cases and interactors
│ │ │ └── user/
│ │ │ ├── interface/ # Abstract use case definitions
│ │ │ │ ├── create_user.usecase.dart
│ │ │ │ └── repository.dart
│ │ │ └── create_user.interactor.dart # Concrete implementations
│ │ └── repository/ # Data access implementations
│ │ └── user/
│ │ ├── memory/
│ │ │ ├── user.repository.dart
│ │ │ └── user.uow.dart # Unit of Work factory
│ │ └── sql/ # Alternative implementations
│ └── your_project.dart # Main library exports
├── test/ # Unit and integration tests
├── example/ # Usage examples
└── doc/ # Documentation
Naming Conventions #
Follow these conventions for consistency:
Use Case and Resource Names
- Use case names: snake_case for both action and resource
('create', 'user')✅('update_status', 'order')✅('bulk_delete', 'document')✅
- Resource names: Always singular
'user'not'users'✅'order'not'orders'✅'document'not'documents'✅
Class Names (PascalCase, Singular)
- Use case interfaces:
[Action][Resource]UsecaseCreateUserUsecase✅UpdateStatusOrderUsecase✅BulkDeleteDocumentUsecase✅
- Interactor implementations:
[Action][Resource]InteractorCreateUserInteractor✅UpdateStatusOrderInteractor✅BulkDeleteDocumentInteractor✅
- Entities:
[Resource]EntityUserEntity✅OrderEntity✅DocumentEntity✅
- DTOs:
[Resource]DtoUserDto✅OrderDto✅DocumentDto✅
- Repositories:
[StorageType][Resource]RepositorySqlUserRepository✅InMemoryOrderRepository✅RestDocumentRepository✅
File and Folder Names
- Match resource names (singular): snake_case for files, resource-based folders
entity/user/user.entity.dart✅usecase/order/create_order.usecase.dart✅repository/document/sql/document.repository.dart✅repository/document/memory/document.repository.dart✅
🚀 Quick Start #
Installation #
Add emkore to your pubspec.yaml:
dependencies:
emkore: ^0.3.2
Basic Usage #
1. Define Your Entity
import 'package:emkore/emkore.dart' as emkore;
class UserEntity extends emkore.Entity<UserEntity> {
final String name;
final String email;
final bool isActive;
UserEntity({
required super.id,
required super.createdAt,
required super.updatedAt,
required super.ownerId,
super.deletedAt,
required this.name,
required this.email,
required this.isActive,
});
@override
UserEntity copyWith({
String? id,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? deletedAt,
String? ownerId,
String? name,
String? email,
bool? isActive,
}) {
return UserEntity(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
ownerId: ownerId ?? this.ownerId,
name: name ?? this.name,
email: email ?? this.email,
isActive: isActive ?? this.isActive,
);
}
}
2. Create Use Case Input/Output
class CreateUserInput {
final String name;
final String email;
final emkore.ResourceScope owner;
CreateUserInput({
required this.name,
required this.email,
required this.owner,
});
factory CreateUserInput.fromJson(emkore.JSON json) {
return CreateUserInput(
name: json.getString('name'),
email: json.getString('email'),
owner: emkore.ResourceScope.values.firstWhere(
(s) => s.name == json.getString('owner'),
orElse: () => emkore.ResourceScope.business,
),
);
}
emkore.JSON toJson() {
return emkore.JSON.from({
'name': name,
'email': email,
'owner': owner.name,
});
}
static emkore.ValidationResult validate(CreateUserInput input) {
return emkore.ValidatorBuilder()
.string('name', input.name, maxLength: 100, required: true)
.email('email', input.email, required: true)
.enumField('owner', input.owner,
options: emkore.ResourceScope.values, required: true)
.build();
}
}
// Return simple DTO/JSON instead of Entity
typedef CreateUserOutput = Map<String, dynamic>;
3. Define Use Case Interface
abstract base class CreateUserUsecase
extends emkore.Usecase<CreateUserInput, CreateUserOutput> {
@override
emkore.UsecaseName get usecaseName => ('create', 'user');
@override
List<emkore.Permission> get requiredPermissions => [
emkore.Permission(resource: 'user', action: 'create'),
];
// performExecute is inherited from Usecase base class
}
4. Implement the Interactor
class CreateUserInteractor extends CreateUserUsecase {
final emkore.UnitOfWorkFactory _uowFactory;
CreateUserInteractor(this._uowFactory);
@override
Future<CreateUserOutput> performExecute({
required emkore.Actor actor,
required CreateUserInput input,
}) async {
final validation = CreateUserInput.validate(input);
if (!validation.isValid) {
throw emkore.ValidationException(validation.errors.first);
}
// Use UnitOfWork for transaction support and repository access
return await _uowFactory.transaction(actor.businessId, (uow) async {
// Create entity with proper ownership
final user = UserEntity(
id: emkore.Entity.generateId('USR1'),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
ownerId: input.owner == emkore.ResourceScope.business
? actor.businessId
: actor.id,
name: input.name,
email: input.email,
isActive: true,
);
// Create via repository and return DTO
final repository = getRepository(uow); // Your UoW implementation provides repositories
final createdUser = await repository.create(user);
// Return DTO/JSON instead of Entity
return {
'id': createdUser.id,
'name': createdUser.name,
'email': createdUser.email,
'isActive': createdUser.isActive,
'createdAt': createdUser.createdAt.toIso8601String(),
};
});
}
}
5. Set Up Actor with Permissions
class MyActor extends emkore.Actor {
@override
final String id;
@override
final String businessId;
@override
final String token;
MyActor({required this.id, required this.businessId, required this.token}) {
permissions = [
emkore.Permission(resource: 'user', action: 'create'),
emkore.Permission(resource: 'user', action: 'read'),
// Add more permissions as needed
];
}
}
6. Execute the Interactor
void main() async {
final uowFactory = MyUnitOfWorkFactory(); // Your UoW implementation
final createUser = CreateUserInteractor(uowFactory);
final actor = MyActor(
id: 'user_123',
businessId: 'company_abc',
token: 'jwt_token_here'
);
final input = CreateUserInput(
name: 'John Doe',
email: 'john@example.com',
owner: emkore.ResourceScope.business,
);
try {
final userDto = await createUser.execute(actor: actor, input: input);
print('Created user: ${userDto['name']} (${userDto['id']})');
} catch (e) {
print('Error: $e');
}
}
🏗️ Architecture Overview #
Emkore implements Clean Architecture principles with these core abstractions:
┌─────────────────────────────────────────────┐
│ UI Layer │
├─────────────────────────────────────────────┤
│ Use Cases │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Interactors │←│ Interceptors │ │
│ │ │ │ • Authorization │ │
│ │ │ │ • Logging │ │
│ │ │ │ • Performance │ │
│ └─────────────┘ │ • Audit │ │
│ └─────────────────────────┘ │
├─────────────────────────────────────────────┤
│ Domain Layer │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Entities │ │ Permissions │ │
│ │ │ │ │ │
│ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────┤
│ Infrastructure │
│ Repositories │
└─────────────────────────────────────────────┘
✨ Key Features #
🔐 Built-in Authorization #
// Fine-grained permissions with constraints
final permission = emkore.Permission(
resource: 'document',
action: 'update',
constraints: emkore.ResourceConstraints(
scope: emkore.ResourceScope.business,
businessId: 'company_123',
),
);
// Automatic authorization checking in use cases
abstract class UpdateDocumentUsecase extends emkore.Usecase<Input, Output> {
@override
List<emkore.Permission> get requiredPermissions => [permission];
}
📊 Interceptor Pipeline #
// Add cross-cutting concerns to any interactor
final interactor = CreateUserInteractor(uowFactory);
// Add local interceptors (specific to this interactor instance)
interactor.use([
emkore.LoggerInterceptor<CreateUserInput, CreateUserOutput>(),
emkore.PerformanceInterceptor<CreateUserInput, CreateUserOutput>(),
]);
// Add global interceptors (apply to all use cases)
emkore.Usecase.useGlobal([
emkore.AuthorizationInterceptor<dynamic, dynamic>(),
]);
✅ Fluent Validation #
final validation = emkore.ValidatorBuilder()
.string('name', input.name, maxLength: 100, required: true)
.email('email', input.email, required: true)
.money('price', input.price, min: emkore.Money(0), required: true)
.entityId('parentId', input.parentId, 'DOC1', required: false)
.build();
if (!validation.isValid) {
// Handle validation errors with field-specific messages
print(validation.getFieldErrors('email')); // ["Invalid email format"]
}
🔄 JSON Handling #
Use emkore's JSON helper methods instead of direct casting for type safety:
class CreateUserInput {
final String name;
final String email;
final int age;
factory CreateUserInput.fromJson(emkore.JSON json) {
return CreateUserInput(
name: json.getString('name'), // ✅ Type-safe access
email: json.getString('email'), // ✅ With validation
age: json.getInt('age'), // ✅ Automatic conversion
// json['name'] as String, // ❌ Avoid direct casting
);
}
emkore.JSON toJson() {
return emkore.JSON.from({
'name': name,
'email': email,
'age': age,
});
}
}
Available JSON helper methods:
getString(),getOptionalString()getInt(),getOptionalInt()getBool(),getOptionalBool()getDouble(),getOptionalDouble()getList<T>(),getSet<T>()getJSONList()for nested objects
💰 Type-Safe Value Objects #
// Money prevents precision errors and enforces type safety
final price = emkore.Money(1299); // $12.99 in cents
final tax = emkore.Money(104); // $1.04
final total = price + tax; // $14.03
// Only Money can be added to Money - compile-time safety!
// final invalid = price + 5; // ❌ Compile error
📚 Advanced Usage #
Unit of Work Pattern (Recommended) #
The UnitOfWork pattern provides transaction support and repository access:
// Define your UoW factory
class MyUnitOfWorkFactory implements emkore.UnitOfWorkFactory {
@override
Future<T> transaction<T>(
String tenantId,
Future<T> Function(emkore.UnitOfWork) body,
) async {
// Create transaction context for tenant
final uow = MyUnitOfWork(tenantId);
try {
final result = await body(uow);
await uow.commit();
return result;
} catch (e) {
await uow.rollback();
rethrow;
}
}
}
// Your UoW implementation provides repositories
class MyUnitOfWork implements emkore.UnitOfWork {
@override
final String tenantId;
MyUnitOfWork(this.tenantId);
UserRepository get userRepository => UserRepository(tenantId);
OrderRepository get orderRepository => OrderRepository(tenantId);
@override
Future<void> commit() async {
// Commit transaction
}
@override
Future<void> rollback() async {
// Rollback transaction
}
}
Repository Pattern #
Repositories work with DTOs for consistent data transfer:
abstract interface class UserRepository {
Future<UserDto> create(UserDto dto);
Future<UserDto> findById(String id);
Future<List<UserDto>> findByBusinessId(String businessId);
Future<UserDto> update(UserDto dto);
Future<void> delete(String id);
}
// Use case output can be DTO but should convert to plain JSON at API boundaries
typedef CreateUserOutput = UserDto; // DTO for internal use
// At API/UI boundaries, convert DTOs to plain JSON:
final userDto = await createUser.execute(actor: actor, input: input);
final plainJson = userDto.toJson(); // Convert to Map<String, dynamic> for external use
Custom Interceptors #
class MetricsInterceptor implements emkore.Interceptor<Input, Output> {
@override
String get name => 'metrics';
@override
FutureOr<Input> beforeExecute(Input input, emkore.InterceptorContext context) {
// Record metrics before execution
startTimer(context.usecaseName);
return input;
}
@override
FutureOr<Output> afterExecute(Output result, emkore.InterceptorContext context) {
// Record success metrics
recordSuccess(context.usecaseName);
return result;
}
@override
FutureOr<void> onError(Object error, emkore.InterceptorContext context) {
// Record error metrics
recordError(context.usecaseName, error);
}
}
🔧 Configuration #
Multi-tenancy Support #
// Each business gets isolated data
final userRepo = repositoryFactory.createUserRepository(actor.businessId);
// Built-in tenant isolation in all operations
final users = await userRepo.findByBusinessId(actor.businessId);
Permission Hierarchies #
// ResourceScope hierarchy: owned < team < business < all
emkore.ResourceScope.owned // User's own resources
emkore.ResourceScope.team // Team resources
emkore.ResourceScope.business // Business-wide resources
emkore.ResourceScope.all // System-wide resources
📖 Documentation #
- Permission System Guide - Complete authorization documentation
- Validation Guide - Fluent validation API reference
- Development Guide - Clean Architecture implementation details and development guidelines
- Examples - Complete working examples with tests
🤝 Contributing #
This is a foundational library focused on stability and production use. Contributions should:
- Maintain backward compatibility
- Include comprehensive tests
- Follow Clean Architecture principles
- Provide clear documentation
📄 License #
BSD-3-Clause License. See LICENSE for details.
🏢 About :emko #
Emkore is developed by :emko, focused on building foundational tools for scalable software architecture.
Ready to build better software? Add emkore to your project and experience Clean Architecture done right. ✨