Vyuh CDX

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, plural

Best 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 entity

The theme color is used for:

  • App bar backgrounds
  • Action buttons
  • Selection highlights
  • Chart visualizations

Group related entities:

category: 'Laboratory',  // Menu category

Categories 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},
    );
  }
}
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

  1. Consistent Naming - Use clear, descriptive names
  2. Meaningful Icons - Choose icons that represent the entity
  3. Logical Categories - Group related entities together
  4. Helpful Descriptions - Provide context for users
  5. Dynamic Help - Update help content without code changes

API Best Practices

  1. Type Safety - Always use generic types correctly
  2. Error Handling - Handle specific errors gracefully
  3. Caching - Implement caching for better performance
  4. Validation - Validate data before API calls
  5. 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