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]Usecase
CreateUserUsecase
βUpdateStatusOrderUsecase
βBulkDeleteDocumentUsecase
β
- Interactor implementations:
[Action][Resource]Interactor
CreateUserInteractor
βUpdateStatusOrderInteractor
βBulkDeleteDocumentInteractor
β
- Entities:
[Resource]Entity
UserEntity
βOrderEntity
βDocumentEntity
β
- DTOs:
[Resource]Dto
UserDto
βOrderDto
βDocumentDto
β
- Repositories:
[StorageType][Resource]Repository
SqlUserRepository
β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. β¨