Metadata & API
Entity metadata and API implementation patterns for data operations
Entity Metadata and API Implementation
This guide covers the two foundational components of the entity system: metadata that describes your entities and the API layer that handles data operations. Together, they form the backbone of entity management.
Entity Metadata
Overview
EntityMetadata provides descriptive information about an entity type, controlling how it appears and behaves throughout the system.
class EntityMetadata {
final String identifier;
final String name;
final String pluralName;
final String? description;
final IconData? icon;
final Color? themeColor;
final String? category;
final EntityRouteBuilder route;
final Future<String?> Function(BuildContext)? getHelp;
}Identifier
The identifier is the unique key for your entity type:
identifier: 'reference_standards', // URL-safe, lowercase, pluralBest practices for identifiers:
- Use lowercase letters and underscores
- Use plural form (e.g., 'users', not 'user')
- Keep consistent with API endpoints
- Never change after deployment
Display Names
Control how your entity appears in the UI:
name: 'Reference Standard', // Singular form
pluralName: 'Reference Standards', // Plural form
description: 'Chemical and biological reference materials',These are used in:
- Page titles
- Navigation menus
- Breadcrumbs
- Search results
- Error messages
Visual Identity
Define the visual appearance:
icon: Icons.science, // Material icon
themeColor: Colors.purple, // Primary color for entityThe theme color is used for:
- App bar backgrounds
- Action buttons
- Selection highlights
- Chart visualizations
Menu Organization
Group related entities:
category: 'Laboratory', // Menu categoryCategories organize entities in:
- Navigation drawer
- Dashboard sections
- Command palette
Route Configuration
The EntityRouteBuilder manages all entity routes:
route: EntityRouteBuilder.fromIdentifier('reference-standards'),This generates standard routes:
/reference-standards- List view/reference-standards/new- Create form/reference-standards/:id- Detail view/reference-standards/:id/edit- Edit form
Custom Route Builder
For non-standard routing:
route: EntityRouteBuilder(
list: (context) => '/lab/standards',
view: (context, id) => '/lab/standards/$id',
edit: (context, id) => '/lab/standards/$id/modify',
create: (context) => '/lab/standards/add',
dashboard: (context) => '/lab/dashboard/standards',
)Help Integration
Provide context-sensitive help:
getHelp: (context) async {
// Load from remote source
final helpContent = await HelpService.loadHelp('reference_standards');
return helpContent;
// Or return inline help
return '''
## Reference Standards
Reference standards are certified materials used for:
- Instrument calibration
- Method validation
- Quality control
### Managing Standards
1. Create new standard with lot information
2. Track expiration dates
3. Record usage history
''';
},Entity API Implementation
Overview
The EntityApi abstract class provides a consistent interface for all entity operations:
abstract class EntityApi<T extends EntityBase> {
final SharedApiClient client;
EntityApi({required this.client});
// Must implement
T fromJson(Map<String, dynamic> json);
// Public API methods
Future<List<T>> list({...});
Future<int> count({...});
Future<T?> byId(String id);
Future<T> createNew(T entity);
Future<T> update(String id, T entity);
Future<void> delete(String id);
// Protected methods to override
Future<List<T>> performList({...});
Future<int> performCount({...});
Future<T?> performGetById(String id);
Future<T> performCreate(T entity);
Future<T> performUpdate(String id, T entity);
Future<void> performDelete(String id);
}Basic Implementation
A minimal API implementation:
class UserApi extends EntityApi<User> {
UserApi({required super.client});
@override
User fromJson(Map<String, dynamic> json) => User.fromJson(json);
}This automatically provides:
- Standard CRUD operations
- Consistent error handling
- Automatic retries
- Response caching
Customizing Operations
Override protected methods for custom behavior:
class UserApi extends EntityApi<User> {
UserApi({required super.client});
@override
User fromJson(Map<String, dynamic> json) => User.fromJson(json);
@override
Future<List<User>> performList({
int? offset,
int? limit,
String? sortBy,
String? sortOrder,
String? search,
}) async {
// Custom endpoint or parameters
final response = await client.get(
'/api/v1/users/active', // Custom endpoint
queryParameters: {
if (offset != null) 'skip': offset.toString(),
if (limit != null) 'take': limit.toString(),
if (sortBy != null) 'orderBy': sortBy,
if (sortOrder != null) 'order': sortOrder,
if (search != null) 'q': search,
'includePermissions': 'true', // Custom parameter
},
);
final List<dynamic> data = response.data['users'];
return data.map((json) => fromJson(json)).toList();
}
@override
Future<User> performCreate(User entity) async {
// Add validation
if (entity.email.isEmpty) {
throw ValidationException('Email is required');
}
// Transform data before sending
final userData = entity.toJson();
userData['createdBy'] = await getCurrentUserId();
final response = await client.post(
'/api/v1/users',
data: userData,
);
return fromJson(response.data);
}
}Advanced API Patterns
Batch Operations
class ProductApi extends EntityApi<Product> {
// ... standard implementation
Future<List<Product>> batchCreate(List<Product> products) async {
final response = await client.post(
'/api/v1/products/batch',
data: {
'products': products.map((p) => p.toJson()).toList(),
},
);
final List<dynamic> created = response.data['created'];
return created.map((json) => fromJson(json)).toList();
}
Future<void> batchUpdate(Map<String, Product> updates) async {
await client.put(
'/api/v1/products/batch',
data: {
'updates': updates.map((id, product) =>
MapEntry(id, product.toJson())
),
},
);
}
}Relationships and Nested Resources
class LocationApi extends EntityApi<Location> {
// ... standard implementation
Future<List<Equipment>> getEquipment(String locationId) async {
final response = await client.get(
'/api/v1/locations/$locationId/equipment',
);
final List<dynamic> data = response.data['equipment'];
return data.map((json) => Equipment.fromJson(json)).toList();
}
Future<void> assignEquipment(
String locationId,
List<String> equipmentIds,
) async {
await client.post(
'/api/v1/locations/$locationId/equipment',
data: {'equipmentIds': equipmentIds},
);
}
}Filtering and Search
class ReferenceStandardApi extends EntityApi<ReferenceStandard> {
// ... standard implementation
Future<List<ReferenceStandard>> searchByProperties({
String? catalogNumber,
String? lotNumber,
DateTime? expiringBefore,
List<String>? categories,
}) async {
final response = await client.get(
'/api/v1/reference-standards/search',
queryParameters: {
if (catalogNumber != null) 'catalogNumber': catalogNumber,
if (lotNumber != null) 'lotNumber': lotNumber,
if (expiringBefore != null)
'expiringBefore': expiringBefore.toIso8601String(),
if (categories != null) 'categories': categories.join(','),
},
);
final List<dynamic> data = response.data['results'];
return data.map((json) => fromJson(json)).toList();
}
}Optimistic Updates
class TaskApi extends EntityApi<Task> {
// ... standard implementation
Future<Task> toggleComplete(String taskId, bool completed) async {
// Optimistically update local cache
final cachedTask = await getCachedTask(taskId);
if (cachedTask != null) {
cachedTask.completed = completed;
updateCache(cachedTask);
}
try {
final response = await client.patch(
'/api/v1/tasks/$taskId',
data: {'completed': completed},
);
return fromJson(response.data);
} catch (e) {
// Revert optimistic update on failure
if (cachedTask != null) {
cachedTask.completed = !completed;
updateCache(cachedTask);
}
rethrow;
}
}
}Error Handling
The API base class provides standard error handling:
class CustomApi extends EntityApi<CustomEntity> {
@override
Future<CustomEntity> performCreate(CustomEntity entity) async {
try {
return await super.performCreate(entity);
} on DioException catch (e) {
if (e.response?.statusCode == 409) {
throw ConflictException(
'Entity with this identifier already exists',
);
}
// Let base class handle other errors
rethrow;
}
}
}API Client Configuration
The SharedApiClient provides:
class SharedApiClient extends DioClient {
// Automatic token management
// Request/response interceptors
// Error transformation
// Retry logic
// Request cancellation
}Configure for your needs:
final apiClient = SharedApiClient(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
interceptors: [
AuthInterceptor(),
LoggingInterceptor(),
RetryInterceptor(),
],
);Metadata + API Integration
Registration Pattern
Combine metadata and API in entity configuration:
final equipmentConfig = EntityConfiguration<Equipment>(
metadata: EntityMetadata(
identifier: 'equipment',
name: 'Equipment',
pluralName: 'Equipment',
icon: Icons.devices,
themeColor: Colors.blue,
category: 'Assets',
route: EntityRouteBuilder.fromIdentifier('equipment'),
),
api: (client) => EquipmentApi(client: client),
// ... other configuration
);
// Register with Vyuh
vyuh.entity.register<Equipment>(equipmentConfig);Dynamic Metadata
Metadata can be dynamic based on context:
EntityMetadata createUserMetadata(BuildContext context) {
final userRole = context.read<AuthService>().currentUser?.role;
return EntityMetadata(
identifier: 'users',
name: 'User',
pluralName: 'Users',
icon: Icons.person,
themeColor: Colors.indigo,
category: userRole == 'admin' ? 'Administration' : 'Organization',
route: userRole == 'admin'
? AdminUserRouteBuilder()
: StandardUserRouteBuilder(),
getHelp: (context) async {
return userRole == 'admin'
? await loadAdminHelp()
: await loadUserHelp();
},
);
}API Selection
Choose different API implementations:
api: (client) {
final environment = getEnvironment();
switch (environment) {
case Environment.mock:
return MockUserApi();
case Environment.staging:
return StagingUserApi(client: client);
case Environment.production:
return UserApi(client: client);
}
},Best Practices
Metadata Best Practices
- Consistent Naming - Use clear, descriptive names
- Meaningful Icons - Choose icons that represent the entity
- Logical Categories - Group related entities together
- Helpful Descriptions - Provide context for users
- Dynamic Help - Update help content without code changes
API Best Practices
- Type Safety - Always use generic types correctly
- Error Handling - Handle specific errors gracefully
- Caching - Implement caching for better performance
- Validation - Validate data before API calls
- Documentation - Document custom endpoints and behaviors
Testing
Test both metadata and API:
// Test metadata
test('equipment metadata is correct', () {
final metadata = EquipmentConfig.instance.metadata;
expect(metadata.identifier, 'equipment');
expect(metadata.pluralName, 'Equipment');
expect(metadata.icon, Icons.devices);
});
// Test API
test('equipment api creates entity', () async {
final api = EquipmentApi(client: mockClient);
final equipment = Equipment(
id: '123',
schemaType: 'equipment',
name: 'Test Equipment',
);
when(mockClient.post(any, data: anyNamed('data')))
.thenAnswer((_) async => Response(
data: equipment.toJson(),
statusCode: 201,
));
final created = await api.createNew(equipment);
expect(created.name, 'Test Equipment');
});Next: Entity Lifecycle - Complete guide to entity lifecycle management and relationships