Vyuh CDX

Entity Lifecycle

Complete entity lifecycle from creation to deletion, including state management and relationships

Entity Lifecycle Management and Relationships

This comprehensive guide covers the complete lifecycle of entities in the Vyuh Entity System, from creation to deletion, including state management, relationships, and advanced manipulation APIs.

Entity Lifecycle Overview

Every entity in the system goes through distinct lifecycle stages:

Creation → Loading → Active Use → Updates → Archival/Deletion
    ↓         ↓           ↓          ↓            ↓
  Events   Events     Events     Events       Events

Lifecycle Stages

1. Entity Creation

Entities can be created through multiple pathways:

Factory Methods

class Equipment extends EntityBase {
  // Direct constructor
  Equipment({
    required super.id,
    required super.schemaType,
    required this.name,
    required this.serialNumber,
    // ... other fields
  });
  
  // Factory for new entities
  factory Equipment.create({
    required String name,
    required String serialNumber,
    String? description,
  }) {
    return Equipment(
      id: const Uuid().v4(),
      schemaType: 'equipment',
      name: name,
      serialNumber: serialNumber,
      description: description,
      createdAt: DateTime.now(),
      layout: 'default',
      modifiers: const {},
    );
  }
  
  // Factory from template
  factory Equipment.fromTemplate(EquipmentTemplate template) {
    return Equipment(
      id: const Uuid().v4(),
      schemaType: 'equipment',
      name: template.name,
      serialNumber: '', // To be assigned
      category: template.category,
      specifications: template.defaultSpecs,
      createdAt: DateTime.now(),
    );
  }
}

Builder Pattern

class EquipmentBuilder {
  String? _name;
  String? _serialNumber;
  String? _locationId;
  Map<String, dynamic> _specifications = {};
  
  EquipmentBuilder withName(String name) {
    _name = name;
    return this;
  }
  
  EquipmentBuilder withSerialNumber(String serialNumber) {
    _serialNumber = serialNumber;
    return this;
  }
  
  EquipmentBuilder withLocation(String locationId) {
    _locationId = locationId;
    return this;
  }
  
  EquipmentBuilder withSpecification(String key, dynamic value) {
    _specifications[key] = value;
    return this;
  }
  
  Equipment build() {
    if (_name == null || _serialNumber == null) {
      throw StateError('Name and serial number are required');
    }
    
    return Equipment(
      id: const Uuid().v4(),
      schemaType: 'equipment',
      name: _name!,
      serialNumber: _serialNumber!,
      locationId: _locationId,
      specifications: _specifications,
      createdAt: DateTime.now(),
    );
  }
}

// Usage
final equipment = EquipmentBuilder()
  .withName('Centrifuge')
  .withSerialNumber('CF-2024-001')
  .withLocation('lab-1')
  .withSpecification('maxRPM', 15000)
  .withSpecification('capacity', '4x100ml')
  .build();

2. Entity Loading

Entities are loaded from various sources:

From JSON

// Standard deserialization
final equipment = Equipment.fromJson(jsonData);

// With validation
Equipment loadEquipmentWithValidation(Map<String, dynamic> json) {
  // Validate required fields
  if (!json.containsKey('serialNumber')) {
    throw ValidationException('Serial number is required');
  }
  
  final equipment = Equipment.fromJson(json);
  
  // Post-load validation
  if (equipment.serialNumber.length < 5) {
    throw ValidationException('Invalid serial number format');
  }
  
  return equipment;
}

Lazy Loading

class LazyLoadedEquipment extends Equipment {
  bool _specificationsLoaded = false;
  Map<String, dynamic>? _cachedSpecifications;
  
  @override
  Map<String, dynamic> get specifications {
    if (!_specificationsLoaded) {
      _loadSpecifications();
    }
    return _cachedSpecifications ?? {};
  }
  
  Future<void> _loadSpecifications() async {
    _cachedSpecifications = await EquipmentApi.loadSpecifications(id);
    _specificationsLoaded = true;
  }
}

3. Active Entity State Management

During active use, entities maintain state and handle changes:

State Tracking

mixin EntityStateTracking on EntityBase {
  final Set<String> _modifiedFields = {};
  Map<String, dynamic>? _originalValues;
  
  void trackChange(String field, dynamic oldValue, dynamic newValue) {
    if (_originalValues == null) {
      _originalValues = {};
    }
    
    if (!_originalValues!.containsKey(field)) {
      _originalValues![field] = oldValue;
    }
    
    if (_originalValues![field] == newValue) {
      _modifiedFields.remove(field);
    } else {
      _modifiedFields.add(field);
    }
  }
  
  bool get hasChanges => _modifiedFields.isNotEmpty;
  
  Set<String> get modifiedFields => Set.unmodifiable(_modifiedFields);
  
  void resetChanges() {
    _modifiedFields.clear();
    _originalValues = null;
  }
}

// Usage in entity
class TrackedUser extends User with EntityStateTracking {
  String _email;
  
  @override
  String get email => _email;
  
  set email(String value) {
    if (_email != value) {
      trackChange('email', _email, value);
      _email = value;
    }
  }
}

Observable Entities

class ObservableEntity<T extends EntityBase> extends ChangeNotifier {
  T _entity;
  
  ObservableEntity(this._entity);
  
  T get entity => _entity;
  
  void update(T Function(T) updater) {
    _entity = updater(_entity);
    notifyListeners();
  }
  
  void updateField<V>(
    V Function(T) getter,
    T Function(T, V) setter,
    V newValue,
  ) {
    final currentValue = getter(_entity);
    if (currentValue != newValue) {
      _entity = setter(_entity, newValue);
      notifyListeners();
    }
  }
}

// Usage
final observableUser = ObservableEntity(user);
observableUser.updateField(
  (u) => u.email,
  (u, email) => u.copyWith(email: email),
  '[email protected]',
);

4. Entity Updates

Entities support various update patterns:

Immutable Updates

extension EquipmentUpdates on Equipment {
  Equipment copyWith({
    String? name,
    String? serialNumber,
    String? locationId,
    EquipmentStatus? status,
    Map<String, dynamic>? specifications,
  }) {
    return Equipment(
      id: id,
      schemaType: schemaType,
      name: name ?? this.name,
      serialNumber: serialNumber ?? this.serialNumber,
      locationId: locationId ?? this.locationId,
      status: status ?? this.status,
      specifications: specifications ?? this.specifications,
      createdAt: createdAt,
      modifiers: modifiers,
    );
  }
}

Partial Updates

class PartialUpdate<T extends EntityBase> {
  final String entityId;
  final Map<String, dynamic> updates;
  final Set<String> removedFields;
  
  PartialUpdate({
    required this.entityId,
    required this.updates,
    this.removedFields = const {},
  });
  
  T apply(T entity) {
    final json = entity.toJson();
    
    // Apply updates
    updates.forEach((key, value) {
      json[key] = value;
    });
    
    // Remove fields
    for (final field in removedFields) {
      json.remove(field);
    }
    
    return entity.constructor.fromJson(json);
  }
}

Batch Updates

class BatchEntityUpdater<T extends EntityBase> {
  final List<T> entities;
  final EntityApi<T> api;
  
  BatchEntityUpdater({
    required this.entities,
    required this.api,
  });
  
  Future<List<T>> updateAll(T Function(T) updater) async {
    final updates = <String, T>{};
    
    for (final entity in entities) {
      final updated = updater(entity);
      if (updated != entity) {
        updates[entity.id] = updated;
      }
    }
    
    if (updates.isEmpty) return entities;
    
    // Batch API call
    final response = await api.batchUpdate(updates);
    return response;
  }
  
  Future<List<T>> updateWhere(
    bool Function(T) predicate,
    T Function(T) updater,
  ) async {
    final toUpdate = entities.where(predicate).toList();
    return updateAll((entity) {
      return toUpdate.contains(entity) ? updater(entity) : entity;
    });
  }
}

5. Entity Deletion

Entities can be deleted with different strategies:

Soft Delete

mixin SoftDeletable on EntityBase {
  bool get isDeleted => modifiers['deleted'] == true;
  DateTime? get deletedAt => modifiers['deletedAt'] != null
    ? DateTime.parse(modifiers['deletedAt'])
    : null;
  String? get deletedBy => modifiers['deletedBy'];
  
  Map<String, dynamic> softDelete({required String userId}) {
    return {
      ...toJson(),
      'modifiers': {
        ...modifiers,
        'deleted': true,
        'deletedAt': DateTime.now().toIso8601String(),
        'deletedBy': userId,
      },
    };
  }
  
  Map<String, dynamic> restore() {
    final updated = Map<String, dynamic>.from(modifiers);
    updated.remove('deleted');
    updated.remove('deletedAt');
    updated.remove('deletedBy');
    
    return {
      ...toJson(),
      'modifiers': updated,
    };
  }
}

Cascade Delete

class CascadeDeleteService {
  Future<void> deleteWithRelated(
    EntityBase entity,
    List<RelationshipDefinition> relationships,
  ) async {
    // Start transaction
    await database.transaction((tx) async {
      // Delete related entities first
      for (final relationship in relationships) {
        if (relationship.onDelete == OnDeleteAction.cascade) {
          await tx.delete(
            relationship.relatedTable,
            where: '${relationship.foreignKey} = ?',
            whereArgs: [entity.id],
          );
        }
      }
      
      // Delete main entity
      await tx.delete(
        entity.schemaType,
        where: 'id = ?',
        whereArgs: [entity.id],
      );
    });
  }
}

Entity Relationships

Defining Relationships

class RelationshipDefinition {
  final String name;
  final String relatedEntity;
  final RelationType type;
  final String? foreignKey;
  final String? inverseForeignKey;
  final OnDeleteAction onDelete;
  final OnUpdateAction onUpdate;
  
  const RelationshipDefinition({
    required this.name,
    required this.relatedEntity,
    required this.type,
    this.foreignKey,
    this.inverseForeignKey,
    this.onDelete = OnDeleteAction.restrict,
    this.onUpdate = OnUpdateAction.cascade,
  });
}

enum RelationType {
  oneToOne,
  oneToMany,
  manyToOne,
  manyToMany,
}

enum OnDeleteAction {
  cascade,
  setNull,
  restrict,
}

One-to-Many Relationships

class Location extends EntityBase {
  // Location has many equipment
  Future<List<Equipment>> get equipment async {
    return await EquipmentApi().list(
      filters: {'locationId': id},
    );
  }
}

class Equipment extends EntityBase {
  final String? locationId;
  
  // Equipment belongs to location
  Future<Location?> get location async {
    if (locationId == null) return null;
    return await LocationApi().byId(locationId!);
  }
}

Many-to-Many Relationships

class User extends EntityBase {
  // User has many roles through user_roles
  Future<List<Role>> get roles async {
    final userRoles = await database.query(
      'user_roles',
      where: 'user_id = ?',
      whereArgs: [id],
    );
    
    final roleIds = userRoles.map((ur) => ur['role_id'] as String).toList();
    return await RoleApi().getByIds(roleIds);
  }
  
  Future<void> addRole(String roleId) async {
    await database.insert('user_roles', {
      'user_id': id,
      'role_id': roleId,
      'assigned_at': DateTime.now().toIso8601String(),
    });
  }
  
  Future<void> removeRole(String roleId) async {
    await database.delete(
      'user_roles',
      where: 'user_id = ? AND role_id = ?',
      whereArgs: [id, roleId],
    );
  }
}

Eager Loading

class EntityLoader<T extends EntityBase> {
  final EntityApi<T> api;
  final List<RelationshipDefinition> relationships;
  
  EntityLoader({
    required this.api,
    required this.relationships,
  });
  
  Future<T> loadWithRelationships(String id) async {
    final entity = await api.byId(id);
    if (entity == null) throw NotFoundException();
    
    final json = entity.toJson();
    
    // Load all relationships
    for (final rel in relationships) {
      switch (rel.type) {
        case RelationType.oneToMany:
          json[rel.name] = await _loadOneToMany(entity, rel);
          break;
        case RelationType.manyToOne:
          json[rel.name] = await _loadManyToOne(entity, rel);
          break;
        case RelationType.manyToMany:
          json[rel.name] = await _loadManyToMany(entity, rel);
          break;
        case RelationType.oneToOne:
          json[rel.name] = await _loadOneToOne(entity, rel);
          break;
      }
    }
    
    return api.fromJson(json);
  }
  
  Future<List<dynamic>> _loadOneToMany(
    T entity,
    RelationshipDefinition rel,
  ) async {
    final relatedApi = EntityRegistry.getApi(rel.relatedEntity);
    return await relatedApi.list(
      filters: {rel.foreignKey ?? '${entity.schemaType}_id': entity.id},
    );
  }
}

Advanced Lifecycle Features

Entity Versioning

mixin Versionable on EntityBase {
  int get version => modifiers['version'] ?? 1;
  
  Map<String, dynamic> incrementVersion() {
    return {
      ...toJson(),
      'modifiers': {
        ...modifiers,
        'version': version + 1,
        'versionedAt': DateTime.now().toIso8601String(),
      },
    };
  }
}

class VersionedEntity<T extends EntityBase> {
  final T current;
  final List<T> history;
  
  VersionedEntity({
    required this.current,
    required this.history,
  });
  
  T? getVersion(int version) {
    return history.firstWhereOrNull((e) => e.version == version);
  }
  
  List<EntityDiff> getDiffs() {
    final diffs = <EntityDiff>[];
    
    for (var i = 1; i < history.length; i++) {
      diffs.add(EntityDiff.between(history[i - 1], history[i]));
    }
    
    return diffs;
  }
}

Entity Cloning

extension EntityCloning<T extends EntityBase> on T {
  T clone({
    String? newId,
    Map<String, dynamic>? overrides,
    bool resetTimestamps = true,
  }) {
    final json = toJson();
    
    // Update ID
    json['id'] = newId ?? const Uuid().v4();
    
    // Reset timestamps
    if (resetTimestamps) {
      json['createdAt'] = DateTime.now().toIso8601String();
      json.remove('updatedAt');
    }
    
    // Apply overrides
    if (overrides != null) {
      json.addAll(overrides);
    }
    
    // Clear version info
    final modifiers = Map<String, dynamic>.from(json['modifiers'] ?? {});
    modifiers.remove('version');
    modifiers.remove('versionedAt');
    json['modifiers'] = modifiers;
    
    return (this as dynamic).fromJson(json) as T;
  }
  
  T deepClone({
    required Map<String, RelationshipDefinition> relationships,
  }) {
    final cloned = clone();
    
    // Clone related entities
    for (final entry in relationships.entries) {
      final rel = entry.value;
      if (rel.type == RelationType.oneToMany) {
        // Clone child entities
        // Implementation depends on your specific needs
      }
    }
    
    return cloned;
  }
}

Import/Export

class EntityExporter<T extends EntityBase> {
  final EntityApi<T> api;
  
  EntityExporter({required this.api});
  
  Future<String> exportToJson(List<String> ids) async {
    final entities = await Future.wait(
      ids.map((id) => api.byId(id)),
    );
    
    final validEntities = entities.whereType<T>().toList();
    
    return jsonEncode({
      'exported_at': DateTime.now().toIso8601String(),
      'entity_type': T.toString(),
      'count': validEntities.length,
      'entities': validEntities.map((e) => e.toJson()).toList(),
    });
  }
  
  Future<String> exportToCsv(
    List<String> ids,
    List<String> fields,
  ) async {
    final entities = await Future.wait(
      ids.map((id) => api.byId(id)),
    );
    
    final csv = StringBuffer();
    
    // Header
    csv.writeln(fields.join(','));
    
    // Data
    for (final entity in entities.whereType<T>()) {
      final json = entity.toJson();
      final values = fields.map((field) {
        final value = json[field];
        // Escape CSV values
        if (value.toString().contains(',')) {
          return '"${value.toString().replaceAll('"', '""')}"';
        }
        return value.toString();
      });
      csv.writeln(values.join(','));
    }
    
    return csv.toString();
  }
}

class EntityImporter<T extends EntityBase> {
  final EntityApi<T> api;
  
  EntityImporter({required this.api});
  
  Future<List<T>> importFromJson(String jsonData) async {
    final data = jsonDecode(jsonData);
    final entities = (data['entities'] as List)
      .map((json) => api.fromJson(json))
      .toList();
    
    // Validate before import
    for (final entity in entities) {
      await validateEntity(entity);
    }
    
    // Import with new IDs
    final imported = <T>[];
    for (final entity in entities) {
      final newEntity = entity.clone();
      final created = await api.createNew(newEntity);
      imported.add(created);
    }
    
    return imported;
  }
}

Lifecycle Hooks

abstract class EntityLifecycleHooks<T extends EntityBase> {
  Future<void> beforeCreate(T entity) async {}
  Future<void> afterCreate(T entity) async {}
  
  Future<void> beforeUpdate(T oldEntity, T newEntity) async {}
  Future<void> afterUpdate(T oldEntity, T newEntity) async {}
  
  Future<void> beforeDelete(T entity) async {}
  Future<void> afterDelete(T entity) async {}
}

class EquipmentLifecycleHooks extends EntityLifecycleHooks<Equipment> {
  @override
  Future<void> beforeCreate(Equipment entity) async {
    // Validate serial number uniqueness
    final existing = await EquipmentApi().findBySerialNumber(
      entity.serialNumber,
    );
    if (existing != null) {
      throw ValidationException('Serial number already exists');
    }
  }
  
  @override
  Future<void> afterCreate(Equipment entity) async {
    // Create initial calibration record
    await CalibrationApi().createInitialRecord(entity.id);
    
    // Send notification
    await NotificationService.notify(
      'New equipment added: ${entity.name}',
    );
  }
  
  @override
  Future<void> beforeDelete(Equipment entity) async {
    // Check if equipment is in use
    final inUse = await EquipmentUsageApi().isInUse(entity.id);
    if (inUse) {
      throw ValidationException('Cannot delete equipment in use');
    }
  }
}

Entity Transformations

abstract class EntityTransformer<S extends EntityBase, T extends EntityBase> {
  T transform(S source);
  S? reverseTransform(T target);
}

class UserToPersonTransformer extends EntityTransformer<User, Person> {
  @override
  Person transform(User user) {
    return Person(
      id: user.id,
      schemaType: 'persons',
      firstName: user.name.split(' ').first,
      lastName: user.name.split(' ').skip(1).join(' '),
      email: user.email,
      phoneNumber: user.phone,
      createdAt: user.createdAt,
    );
  }
  
  @override
  User? reverseTransform(Person person) {
    // Reverse transformation might not always be possible
    return null;
  }
}

Performance Optimization

Entity Caching

class EntityCache<T extends EntityBase> {
  final Map<String, T> _cache = {};
  final Map<String, DateTime> _timestamps = {};
  final Duration ttl;
  
  EntityCache({this.ttl = const Duration(minutes: 5)});
  
  T? get(String id) {
    final timestamp = _timestamps[id];
    if (timestamp != null && 
        DateTime.now().difference(timestamp) > ttl) {
      _cache.remove(id);
      _timestamps.remove(id);
      return null;
    }
    return _cache[id];
  }
  
  void put(T entity) {
    _cache[entity.id] = entity;
    _timestamps[entity.id] = DateTime.now();
  }
  
  void invalidate(String id) {
    _cache.remove(id);
    _timestamps.remove(id);
  }
  
  void clear() {
    _cache.clear();
    _timestamps.clear();
  }
}

Query Optimization

class OptimizedEntityQuery<T extends EntityBase> {
  final EntityApi<T> api;
  
  OptimizedEntityQuery({required this.api});
  
  Stream<List<T>> watchList({
    required QueryOptions options,
    Duration pollInterval = const Duration(seconds: 30),
  }) async* {
    var lastResult = <T>[];
    var lastHash = '';
    
    while (true) {
      try {
        final result = await api.list(
          offset: options.offset,
          limit: options.limit,
          sortBy: options.sortBy,
        );
        
        final currentHash = _computeHash(result);
        if (currentHash != lastHash) {
          lastResult = result;
          lastHash = currentHash;
          yield result;
        }
      } catch (e) {
        // Yield last known good result on error
        yield lastResult;
      }
      
      await Future.delayed(pollInterval);
    }
  }
  
  String _computeHash(List<T> entities) {
    final ids = entities.map((e) => e.id).join(',');
    return ids.hashCode.toString();
  }
}

Best Practices

  1. Immutability - Prefer immutable updates with copyWith
  2. Validation - Validate at every lifecycle stage
  3. Relationships - Define clear relationship patterns
  4. Performance - Use caching and lazy loading appropriately
  5. Hooks - Implement lifecycle hooks for complex logic
  6. Versioning - Track changes for audit and rollback
  7. Error Handling - Handle lifecycle errors gracefully

Next: Layouts and Views - Complete guide to the layout system