Entity Define

📖 16 min read

First, you need to define your data models by extending either DatumEntity or RelationalDatumEntity. These classes provide the core structure for your entities, including properties like id, userId, createdAt, modifiedAt, isDeleted, and version.

Entity Types#

DatumEntity (Non-Relational)#

Use DatumEntity for entities that don't need relationships with other entities. This is the base class for simple data models.

RelationalDatumEntity (Relational)#

Use RelationalDatumEntity for entities that have relationships with other entities. This extends DatumEntity and adds support for defining and managing relationships.

Entity Definition Approaches#

Datum provides multiple ways to define your entities. Choose the approach that best fits your needs:

Use when: You want full Datum integration with built-in relationship support.

DatumEntity (Non-Relational)#

Use DatumEntity for entities that don't need relationships:

   class Task extends DatumEntity {
     @override
     final String id;
     @override
     final String userId;
     final String title;
     final String? description;
     final bool isCompleted;

     const Task({
       required this.id,
       required this.userId,
       required this.title,
       this.description,
       this.isCompleted = false,
       required super.createdAt,
       required super.modifiedAt,
       super.isDeleted = false,
       super.version = 1,
     });

     // Implement required methods...
   }

RelationalDatumEntity (Relational)#

Use RelationalDatumEntity for entities with relationships:

  class User extends RelationalDatumEntity {
    @override
    Map<String, Relation> get relations => {
      'posts': HasMany<Post>(this, 'userId'),
      'profile': HasOne<Profile>(this, 'userId'),
    };

    // Implementation...
  }

Benefits: - Full Datum integration - Built-in relationship support - Type safety - Automatic serialization

Complexity: Medium-High

Use when: You need to add Datum functionality to existing classes or prefer composition over inheritance.

DatumEntityMixin#

Basic mixin with change tracking and serialization:

  class Task with DatumEntityMixin {
    String title;
    String description;
    bool isCompleted;
    DateTime? dueDate;

    Task({
      String? id,
      required this.title,
      this.description = '',
      this.isCompleted = false,
      this.dueDate,
    }) {
      // Initialize with mixin
      this.id = id ?? generateId();
      this.createdAt = DateTime.now();
      this.modifiedAt = DateTime.now();
    }

    @override
    Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
      return {
        'id': id,
        'title': title,
        'description': description,
        'isCompleted': isCompleted,
        'dueDate': dueDate?.toIso8601String(),
        'createdAt': createdAt.toIso8601String(),
        'modifiedAt': modifiedAt.toIso8601String(),
      };
    }

    factory Task.fromMap(Map<String, dynamic> map) {
      return Task(
        id: map['id'],
        title: map['title'],
        description: map['description'],
        isCompleted: map['isCompleted'],
        dueDate: map['dueDate'] != null ? DateTime.parse(map['dueDate']) : null,
      )..createdAt = DateTime.parse(map['createdAt'])
        ..modifiedAt = DateTime.parse(map['modifiedAt']);
    }
  }

RelationDatumEntityMixin#

Mixin with relationship management:

  class Project with RelationDatumEntityMixin {
    String name;
    String description;
    List<String> taskIds;

    Project({
      String? id,
      required this.name,
      this.description = '',
      List<String>? taskIds,
    }) : taskIds = taskIds ?? [] {
      this.id = id ?? generateId();
      this.createdAt = DateTime.now();
      this.modifiedAt = DateTime.now();
    }

    // Relationship management methods...
    Future<void> addTask(String taskId) async {
      if (!taskIds.contains(taskId)) {
        taskIds.add(taskId);
        await updateRelationship('tasks', taskId, RelationshipAction.add);
      }
    }
  }

Benefits:

  • Flexible composition
  • Add Datum features to existing classes
  • Less coupling than inheritance
  • Manual relationship control

Complexity: Low-Medium

Detailed Comparison & Best Practices#

FeatureDatumEntityRelationalDatumEntityDatumEntityMixinRelationDatumEntityMixin
Use CaseSimple entitiesComplex relationshipsSimple with utilitiesUtilities + relationships
Complexity🟢 Low🔴 High🟢 Low🟡 Medium
Relationships❌ None✅ Built-in❌ None🔧 Manual
Inheritance✅ Required✅ Required❌ Not required❌ Not required
Flexibility⚪ Low🟡 Medium🟢 High🟢 High
Performance🟢 Good🟢 Good🟢 Good🟢 Good
Setup Time🟢 Fast🟡 Medium🟢 Fast🟡 Medium

🏗️ Inheritance Approaches#

Best For:

  • New projects from scratch
  • Full Datum ecosystem integration
  • Built-in relationship management
  • Type-safe entity hierarchies

When to Choose:

  • You want comprehensive Datum features
  • Relationships are central to your domain
  • You prefer declarative relationship definitions

🔧 Mixin Approaches#

Best For:

  • Adding Datum to existing classes
  • Composition over inheritance preference
  • Library/framework development
  • Gradual migration scenarios

When to Choose:

  • You have existing class hierarchies
  • You need more control over relationships
  • You're building reusable components

🔄 Migration Between Approaches#

From Mixin to Inheritance:

// Before (Mixin)
class Task with DatumEntityMixin {
  // Implementation
}

// After (Inheritance)
class Task extends DatumEntity {
  // Copy properties and methods from mixin version
  // Add required super constructor calls
  Task({
    required super.id,
    required super.userId,
    required super.createdAt,
    required super.modifiedAt,
    // ... other params
  });
}

From Inheritance to Mixin:

// Before (Inheritance)
class Task extends DatumEntity {
  // Implementation
}

// After (Mixin)
class Task with DatumEntityMixin {
  // Remove extends clause
  // Initialize mixin properties in constructor
  Task() {
    this.id = generateId();
    this.createdAt = DateTime.now();
    this.modifiedAt = DateTime.now();
  }
}

💡 Pro Tip: Start with inheritance approaches for new projects, and use mixins when you need to integrate Datum into existing codebases or prefer more flexible composition patterns.

Implementation Best Practices#

Core Requirements#

  1. Always implement toDatumMap() and fromMap()

    @override
    Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
      // Required for serialization
    }
    
    factory EntityName.fromMap(Map<String, dynamic> map) {
      // Required for deserialization
    }
    
  2. Use meaningful relationship names

    // Good
    'author': BelongsTo<User>(this, 'userId'),
    'comments': HasMany<Comment>(this, 'postId'),
    
    // Avoid
    'rel1': BelongsTo<User>(this, 'userId'),
    'rel2': HasMany<Comment>(this, 'postId'),
    

Advanced Patterns#

  1. Validate relationships before saving

    Future<void> saveEntity(MyEntity entity) async {
      await entity.validateRelationships();
      await manager.save(entity, userId: userId);
    }
    
  2. Handle cascading deletes carefully

    @override
    Future<void> beforeDelete() async {
      // Clean up related entities
      for (final relatedId in relatedIds) {
        await deleteRelatedEntity<RelatedEntity>(relatedId);
      }
    }
    
  3. Use lazy loading for performance

    // Load relationships on demand
    Future<List<Comment>> getComments() async {
      return await getRelatedEntities<Comment>('comments');
    }
    

Performance Considerations#

  • Inheritance approaches have slightly better performance due to direct method calls
  • Mixin approaches have minimal performance overhead but offer more flexibility
  • Relationship-heavy entities benefit from built-in relationship management
  • Large datasets should use lazy loading to avoid memory issues

Testing Recommendations#

void main() {
  group('Entity Tests', () {
    test('should serialize correctly', () {
      final entity = Task(title: 'Test Task');
      final map = entity.toDatumMap();
      expect(map['title'], equals('Test Task'));
    });

    test('should deserialize correctly', () {
      final map = {'id': '1', 'title': 'Test', 'userId': 'user1'};
      final entity = Task.fromMap(map);
      expect(entity.title, equals('Test'));
    });

    test('should handle relationships', () async {
      final project = Project(name: 'Test Project');
      await project.addTask('task1');
      expect(project.taskIds, contains('task1'));
    });
  });
}

Next Steps#

Now that you've defined your entities, you can:

  1. Set up adapters: Implement local and remote adapters for your entities
  2. Initialize Datum: Configure and initialize the Datum system
  3. Work with relationships: Learn about defining and using entity relationships (if using RelationalDatumEntity)
  4. Query data: Learn how to query and filter your data

Example: Non-Relational Entity#

Below is an example of a Task entity using DatumEntity (non-relational). Your entities will likely have more specific fields relevant to your application.

import 'package:datum/datum.dart';
import 'dart:convert'; // For json.encode and json.decode
import 'dart:math'; // For Random
import 'package:supabase_flutter/supabase_flutter.dart'; // Example for userId

class Task extends DatumEntity {
  @override
  final String id;

  @override
  final String userId;

  final String title;

  final String? description;

  final bool isCompleted;

  @override
  final DateTime createdAt;

  @override
  final DateTime modifiedAt;

  @override
  final bool isDeleted;

  @override
  final int version;
  const Task({
    required this.id,
    required this.userId,
    required this.title,
    this.description,
    this.isCompleted = false,
    required this.createdAt,
    required this.modifiedAt,
    this.isDeleted = false,
    this.version = 1,
  });

  @override
  Task copyWith({
    String? id,
    String? userId,
    String? title,
    String? description,
    bool? isCompleted,
    DateTime? createdAt,
    DateTime? modifiedAt,
    bool? isDeleted,
    int? version,
  }) {
    // Determine if any field is being changed.
    final bool hasChanges = id != null ||
        userId != null ||
        title != null ||
        description != null ||
        isCompleted != null ||
        createdAt != null ||
        modifiedAt != null ||
        isDeleted != null;

    return Task(
      id: id ?? this.id,
      userId: userId ?? this.userId,
      title: title ?? this.title,
      description: description ?? this.description,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt ?? this.createdAt,
      // Always update modifiedAt if there are changes.
      modifiedAt: (modifiedAt ?? this.modifiedAt),
      isDeleted: isDeleted ?? this.isDeleted,
      // If a version is explicitly passed, use it. Otherwise, increment if there are changes.
      version: version ?? (hasChanges ? this.version + 1 : this.version),
    );
  }

  @override
  Map<String, dynamic>? diff(DatumEntity oldVersion) {
    if (oldVersion is! Task) return toDatumMap();

    final diff = <String, dynamic>{};

    if (title != oldVersion.title) {
      diff['title'] = title;
    }
    if (description != oldVersion.description) {
      diff['description'] = description;
    }
    if (isCompleted != oldVersion.isCompleted) {
      diff['isCompleted'] = isCompleted;
    }
    if (isDeleted != oldVersion.isDeleted) {
      diff['isDeleted'] = isDeleted;
    }

    // Only include modification details if there are other changes
    if (diff.isNotEmpty) {
      diff['modifiedAt'] = modifiedAt.toIso8601String();
      diff['version'] = version;
    }

    return diff.isEmpty ? null : diff;
  }

  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = {
      'id': id,
      'userId': userId,
      'title': title,
      'description': description,
      'isCompleted': isCompleted,
      'isDeleted': isDeleted,
      'version': version,
    };

    if (target == MapTarget.remote) {
      map['createdAt'] = createdAt.toIso8601String();
      map['modifiedAt'] = modifiedAt.toIso8601String();
    } else {
      map['createdAt'] = createdAt.millisecondsSinceEpoch;
      map['modifiedAt'] = modifiedAt.millisecondsSinceEpoch;
    }
    return map;
  }

  factory Task.fromMap(Map<String, dynamic> map) {
    return Task(
      id: (map['id'] ?? '') as String,
      userId: (map['userId'] ?? map['user_id'] ?? '') as String,
      title: (map['title'] ?? '') as String,
      description: map['description'] as String?,
      isCompleted: (map['isCompleted'] ?? map['is_completed'] ?? false) as bool,
      createdAt: _parseDate(map['createdAt'] ?? map['created_at']),
      modifiedAt: _parseDate(map['modifiedAt'] ?? map['modified_at']),
      isDeleted: (map['isDeleted'] ?? map['is_deleted'] ?? false) as bool,
      version: (map['version'] ?? 1) as int,
    );
  }

  static DateTime _parseDate(dynamic dateValue) {
    if (dateValue is int) {
      return DateTime.fromMillisecondsSinceEpoch(dateValue);
    }
    if (dateValue is String) {
      return DateTime.tryParse(dateValue) ??
          DateTime.fromMillisecondsSinceEpoch(0);
    }
    return DateTime.fromMillisecondsSinceEpoch(0);
  }

  static Task create({required String title, String? description}) {
    final now = DateTime.now();
    // This is an example using Supabase for user ID. Adjust as per your auth solution.
    final userId = Supabase.instance.client.auth.currentUser?.id;
    if (userId == null) {
      throw Exception('Cannot create task: user is not logged in.');
    }
    return Task(
      id: '${now.millisecondsSinceEpoch}${Random().nextInt(9999)}',
      userId: userId,
      title: title,
      description: description,
      isCompleted: false,
      createdAt: now,
      modifiedAt: now,
    );
  }

  String toJson() => json.encode(toDatumMap());

  factory Task.fromJson(String source) =>
      Task.fromMap(json.decode(source) as Map<String, dynamic>);

  @override
  String toString() {
    return 'Task(id: $id, userId: $userId, title: $title, description: $description, isCompleted: $isCompleted, createdAt: $createdAt, modifiedAt: $modifiedAt, isDeleted: $isDeleted, version: $version)';
  }

  @override
  bool operator ==(covariant Task other) {
    if (identical(this, other)) return true;

    return other.id == id &&
        other.userId == userId &&
        other.title == title &&
        other.isCompleted == isCompleted &&
        other.description == description &&
        other.createdAt == createdAt &&
        other.modifiedAt == modifiedAt &&
        other.isDeleted == isDeleted &&
        other.version == version;
  }

  @override
  int get hashCode {
    return id.hashCode ^
        userId.hashCode ^
        title.hashCode ^
        isCompleted.hashCode ^
        description.hashCode ^
        createdAt.hashCode ^
        modifiedAt.hashCode ^
        isDeleted.hashCode ^
        version.hashCode;
  }
}

Example: Relational Entity#

Below is an example of entities using RelationalDatumEntity to demonstrate relationships between User, Post, and Comment entities.

import 'package:datum/datum.dart';

// User entity with relationships
class User extends RelationalDatumEntity {
  @override
  final String id;

  @override
  final String userId;

  final String name;
  final String email;

  @override
  final DateTime createdAt;

  @override
  final DateTime modifiedAt;

  @override
  final bool isDeleted;

  @override
  final int version;

  const User({
    required this.id,
    required this.userId,
    required this.name,
    required this.email,
    required this.createdAt,
    required this.modifiedAt,
    this.isDeleted = false,
    this.version = 1,
  });

  @override
  Map<String, Relation> get relations => {
    'posts': HasMany<Post>(this, 'userId'),
    'profile': HasOne<Profile>(this, 'userId'),
  };

  @override
  User copyWith({
    String? id,
    String? userId,
    String? name,
    String? email,
    DateTime? createdAt,
    DateTime? modifiedAt,
    bool? isDeleted,
    int? version,
  }) {
    final bool hasChanges = id != null || userId != null || name != null ||
        email != null || createdAt != null || modifiedAt != null ||
        isDeleted != null;

    return User(
      id: id ?? this.id,
      userId: userId ?? this.userId,
      name: name ?? this.name,
      email: email ?? this.email,
      createdAt: createdAt ?? this.createdAt,
      modifiedAt: hasChanges ? DateTime.now() : (modifiedAt ?? this.modifiedAt),
      isDeleted: isDeleted ?? this.isDeleted,
      version: version ?? (hasChanges ? this.version + 1 : this.version),
    );
  }

  @override
  Map<String, dynamic>? diff(DatumEntityInterface oldVersion) {
    if (oldVersion is! User) return toDatumMap();

    final diff = <String, dynamic>{};
    if (name != oldVersion.name) diff['name'] = name;
    if (email != oldVersion.email) diff['email'] = email;
    if (isDeleted != oldVersion.isDeleted) diff['isDeleted'] = isDeleted;

    if (diff.isNotEmpty) {
      diff['modifiedAt'] = modifiedAt.toIso8601String();
      diff['version'] = version;
    }
    return diff.isEmpty ? null : diff;
  }

  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = {
      'id': id,
      'userId': userId,
      'name': name,
      'email': email,
      'isDeleted': isDeleted,
      'version': version,
    };

    if (target == MapTarget.remote) {
      map['createdAt'] = createdAt.toIso8601String();
      map['modifiedAt'] = modifiedAt.toIso8601String();
    } else {
      map['createdAt'] = createdAt.millisecondsSinceEpoch;
      map['modifiedAt'] = modifiedAt.millisecondsSinceEpoch;
    }
    return map;
  }

  factory User.fromMap(Map<String, dynamic> map) {
    return User(
      id: map['id'] as String,
      userId: map['userId'] as String,
      name: map['name'] as String,
      email: map['email'] as String,
      createdAt: _parseDate(map['createdAt'] ?? map['created_at']),
      modifiedAt: _parseDate(map['modifiedAt'] ?? map['modified_at']),
      isDeleted: (map['isDeleted'] ?? false) as bool,
      version: (map['version'] ?? 1) as int,
    );
  }

  static DateTime _parseDate(dynamic dateValue) {
    if (dateValue is int) return DateTime.fromMillisecondsSinceEpoch(dateValue);
    if (dateValue is String) return DateTime.tryParse(dateValue) ?? DateTime.now();
    return DateTime.now();
  }
}

// Post entity with BelongsTo relationship
class Post extends RelationalDatumEntity {
  @override
  final String id;

  @override
  final String userId; // Foreign key to User

  final String title;
  final String content;

  @override
  final DateTime createdAt;

  @override
  final DateTime modifiedAt;

  @override
  final bool isDeleted;

  @override
  final int version;

  const Post({
    required this.id,
    required this.userId,
    required this.title,
    required this.content,
    required this.createdAt,
    required this.modifiedAt,
    this.isDeleted = false,
    this.version = 1,
  });

  @override
  Map<String, Relation> get relations => {
    'author': BelongsTo<User>(this, 'userId'),
    'comments': HasMany<Comment>(this, 'postId'),
  };

  @override
  Post copyWith({
    String? id,
    String? userId,
    String? title,
    String? content,
    DateTime? createdAt,
    DateTime? modifiedAt,
    bool? isDeleted,
    int? version,
  }) {
    final bool hasChanges = id != null || userId != null || title != null ||
        content != null || createdAt != null || modifiedAt != null ||
        isDeleted != null;

    return Post(
      id: id ?? this.id,
      userId: userId ?? this.userId,
      title: title ?? this.title,
      content: content ?? this.content,
      createdAt: createdAt ?? this.createdAt,
      modifiedAt: hasChanges ? DateTime.now() : (modifiedAt ?? this.modifiedAt),
      isDeleted: isDeleted ?? this.isDeleted,
      version: version ?? (hasChanges ? this.version + 1 : this.version),
    );
  }

  @override
  Map<String, dynamic>? diff(DatumEntityInterface oldVersion) {
    if (oldVersion is! Post) return toDatumMap();

    final diff = <String, dynamic>{};
    if (title != oldVersion.title) diff['title'] = title;
    if (content != oldVersion.content) diff['content'] = content;
    if (userId != oldVersion.userId) diff['userId'] = userId;
    if (isDeleted != oldVersion.isDeleted) diff['isDeleted'] = isDeleted;

    if (diff.isNotEmpty) {
      diff['modifiedAt'] = modifiedAt.toIso8601String();
      diff['version'] = version;
    }
    return diff.isEmpty ? null : diff;
  }

  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = {
      'id': id,
      'userId': userId,
      'title': title,
      'content': content,
      'isDeleted': isDeleted,
      'version': version,
    };

    if (target == MapTarget.remote) {
      map['createdAt'] = createdAt.toIso8601String();
      map['modifiedAt'] = modifiedAt.toIso8601String();
    } else {
      map['createdAt'] = createdAt.millisecondsSinceEpoch;
      map['modifiedAt'] = modifiedAt.millisecondsSinceEpoch;
    }
    return map;
  }

  factory Post.fromMap(Map<String, dynamic> map) {
    return Post(
      id: map['id'] as String,
      userId: map['userId'] as String,
      title: map['title'] as String,
      content: map['content'] as String,
      createdAt: _parseDate(map['createdAt'] ?? map['created_at']),
      modifiedAt: _parseDate(map['modifiedAt'] ?? map['modified_at']),
      isDeleted: (map['isDeleted'] ?? false) as bool,
      version: (map['version'] ?? 1) as int,
    );
  }

  static DateTime _parseDate(dynamic dateValue) {
    if (dateValue is int) return DateTime.fromMillisecondsSinceEpoch(dateValue);
    if (dateValue is String) return DateTime.tryParse(dateValue) ?? DateTime.now();
    return DateTime.now();
  }
}

// Comment entity with BelongsTo relationship
class Comment extends RelationalDatumEntity {
  @override
  final String id;

  @override
  final String userId;

  final String postId; // Foreign key to Post
  final String content;

  @override
  final DateTime createdAt;

  @override
  final DateTime modifiedAt;

  @override
  final bool isDeleted;

  @override
  final int version;

  const Comment({
    required this.id,
    required this.userId,
    required this.postId,
    required this.content,
    required this.createdAt,
    required this.modifiedAt,
    this.isDeleted = false,
    this.version = 1,
  });

  @override
  Map<String, Relation> get relations => {
    'author': BelongsTo<User>(this, 'userId'),
    'post': BelongsTo<Post>(this, 'postId'),
  };

  @override
  Comment copyWith({
    String? id,
    String? userId,
    String? postId,
    String? content,
    DateTime? createdAt,
    DateTime? modifiedAt,
    bool? isDeleted,
    int? version,
  }) {
    final bool hasChanges = id != null || userId != null || postId != null ||
        content != null || createdAt != null || modifiedAt != null ||
        isDeleted != null;

    return Comment(
      id: id ?? this.id,
      userId: userId ?? this.userId,
      postId: postId ?? this.postId,
      content: content ?? this.content,
      createdAt: createdAt ?? this.createdAt,
      modifiedAt: hasChanges ? DateTime.now() : (modifiedAt ?? this.modifiedAt),
      isDeleted: isDeleted ?? this.isDeleted,
      version: version ?? (hasChanges ? this.version + 1 : this.version),
    );
  }

  @override
  Map<String, dynamic>? diff(DatumEntityInterface oldVersion) {
    if (oldVersion is! Comment) return toDatumMap();

    final diff = <String, dynamic>{};
    if (content != oldVersion.content) diff['content'] = content;
    if (postId != oldVersion.postId) diff['postId'] = postId;
    if (userId != oldVersion.userId) diff['userId'] = userId;
    if (isDeleted != oldVersion.isDeleted) diff['isDeleted'] = isDeleted;

    if (diff.isNotEmpty) {
      diff['modifiedAt'] = modifiedAt.toIso8601String();
      diff['version'] = version;
    }
    return diff.isEmpty ? null : diff;
  }

  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = {
      'id': id,
      'userId': userId,
      'postId': postId,
      'content': content,
      'isDeleted': isDeleted,
      'version': version,
    };

    if (target == MapTarget.remote) {
      map['createdAt'] = createdAt.toIso8601String();
      map['modifiedAt'] = modifiedAt.toIso8601String();
    } else {
      map['createdAt'] = createdAt.millisecondsSinceEpoch;
      map['modifiedAt'] = modifiedAt.millisecondsSinceEpoch;
    }
    return map;
  }

  factory Comment.fromMap(Map<String, dynamic> map) {
    return Comment(
      id: map['id'] as String,
      userId: map['userId'] as String,
      postId: map['postId'] as String,
      content: map['content'] as String,
      createdAt: _parseDate(map['createdAt'] ?? map['created_at']),
      modifiedAt: _parseDate(map['modifiedAt'] ?? map['modified_at']),
      isDeleted: (map['isDeleted'] ?? false) as bool,
      version: (map['version'] ?? 1) as int,
    );
  }

  static DateTime _parseDate(dynamic dateValue) {
    if (dateValue is int) return DateTime.fromMillisecondsSinceEpoch(dateValue);
    if (dateValue is String) return DateTime.tryParse(dateValue) ?? DateTime.now();
    return DateTime.now();
  }
}

// Profile entity for HasOne relationship
class Profile extends RelationalDatumEntity {
  @override
  final String id;

  @override
  final String userId; // Foreign key to User

  final String bio;
  final String avatarUrl;

  @override
  final DateTime createdAt;

  @override
  final DateTime modifiedAt;

  @override
  final bool isDeleted;

  @override
  final int version;

  const Profile({
    required this.id,
    required this.userId,
    required this.bio,
    required this.avatarUrl,
    required this.createdAt,
    required this.modifiedAt,
    this.isDeleted = false,
    this.version = 1,
  });

  @override
  Map<String, Relation> get relations => {
    'user': BelongsTo<User>(this, 'userId'),
  };

  @override
  Profile copyWith({
    String? id,
    String? userId,
    String? bio,
    String? avatarUrl,
    DateTime? createdAt,
    DateTime? modifiedAt,
    bool? isDeleted,
    int? version,
  }) {
    final bool hasChanges = id != null || userId != null || bio != null ||
        avatarUrl != null || createdAt != null || modifiedAt != null ||
        isDeleted != null;

    return Profile(
      id: id ?? this.id,
      userId: userId ?? this.userId,
      bio: bio ?? this.bio,
      avatarUrl: avatarUrl ?? this.avatarUrl,
      createdAt: createdAt ?? this.createdAt,
      modifiedAt: hasChanges ? DateTime.now() : (modifiedAt ?? this.modifiedAt),
      isDeleted: isDeleted ?? this.isDeleted,
      version: version ?? (hasChanges ? this.version + 1 : this.version),
    );
  }

  @override
  Map<String, dynamic>? diff(DatumEntityInterface oldVersion) {
    if (oldVersion is! Profile) return toDatumMap();

    final diff = <String, dynamic>{};
    if (bio != oldVersion.bio) diff['bio'] = bio;
    if (avatarUrl != oldVersion.avatarUrl) diff['avatarUrl'] = avatarUrl;
    if (userId != oldVersion.userId) diff['userId'] = userId;
    if (isDeleted != oldVersion.isDeleted) diff['isDeleted'] = isDeleted;

    if (diff.isNotEmpty) {
      diff['modifiedAt'] = modifiedAt.toIso8601String();
      diff['version'] = version;
    }
    return diff.isEmpty ? null : diff;
  }

  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = {
      'id': id,
      'userId': userId,
      'bio': bio,
      'avatarUrl': avatarUrl,
      'isDeleted': isDeleted,
      'version': version,
    };

    if (target == MapTarget.remote) {
      map['createdAt'] = createdAt.toIso8601String();
      map['modifiedAt'] = modifiedAt.toIso8601String();
    } else {
      map['createdAt'] = createdAt.millisecondsSinceEpoch;
      map['modifiedAt'] = modifiedAt.millisecondsSinceEpoch;
    }
    return map;
  }

  factory Profile.fromMap(Map<String, dynamic> map) {
    return Profile(
      id: map['id'] as String,
      userId: map['userId'] as String,
      bio: map['bio'] as String,
      avatarUrl: map['avatarUrl'] as String,
      createdAt: _parseDate(map['createdAt'] ?? map['created_at']),
      modifiedAt: _parseDate(map['modifiedAt'] ?? map['modified_at']),
      isDeleted: (map['isDeleted'] ?? false) as bool,
      version: (map['version'] ?? 1) as int,
    );
  }

  static DateTime _parseDate(dynamic dateValue) {
    if (dateValue is int) return DateTime.fromMillisecondsSinceEpoch(dateValue);
    if (dateValue is String) return DateTime.tryParse(dateValue) ?? DateTime.now();
    return DateTime.now();
  }
}