generateForAnnotatedElement method

  1. @override
Future<String> generateForAnnotatedElement(
  1. Element element,
  2. ConstantReader annotation,
  3. BuildStep buildStep
)

Implement to return source code to generate for element.

This method is invoked based on finding elements annotated with an instance of T. The annotation is provided as a ConstantReader.

Supported return values include a single String or multiple String instances within an Iterable or Stream. It is also valid to return a Future of String, Iterable, or Stream. When multiple values are returned through an iterable or stream they will be deduplicated. Typically each value will be an independent unit of code and the deduplication prevents re-defining the same member multiple times. For example if multiple annotated elements may need a specific utility method available it can be output for each one, and the single deduplicated definition can be shared.

Implementations should return null when no content is generated. Empty or whitespace-only String instances are also ignored.

Implementation

@override
Future<String> generateForAnnotatedElement(
  Element element,
  ConstantReader annotation,
  BuildStep buildStep,
) async {
  if (element is! ClassElement) {
    throw InvalidGenerationSourceError(
      '@Db can only be applied to classes',
      element: element,
    );
  }

  final className = element.name;
  final migrationVersion = annotation.peek('migrationVersion')?.intValue ?? 1;
  final sqlDialectValue = annotation.peek('sqlDialect')?.objectValue;
  final dbName =
      annotation.peek('name')?.stringValue ?? className!.toLowerCase();

  // Extract DbConfig from annotation
  final configInfo = _extractDbConfig(annotation);

  // Determine SQL dialect
  String sqlDialect = 'postgresql';
  if (sqlDialectValue != null) {
    final dialectName = sqlDialectValue.getField('_name')?.toStringValue();
    if (dialectName != null) {
      sqlDialect = dialectName;
    }
  } else if (configInfo != null) {
    sqlDialect = configInfo.dbType;
  }

  // Get schema storage path
  final inputPath = buildStep.inputId.path;
  final packageRoot = inputPath.split('lib/').first;
  final schemaFilePath = '$packageRoot.dorm/$dbName.schema.json';

  // Extract entity types and their schema info from annotation
  final entitiesReader = annotation.peek('entities');
  final entities = <_EntityInfo>[];
  final manyToManyRelations = <_ManyToManyInfo>[];
  final entityElements = <String, ClassElement>{};
  final entityToTableName = <String, String>{};

  // Collect OneToMany relationships to add FK columns to target entities
  final oneToManyRelations = <_OneToManyInfo>[];

  if (entitiesReader != null && !entitiesReader.isNull) {
    final entityList = entitiesReader.listValue;

    // First pass: collect all entity elements and their table names
    for (final entityValue in entityList) {
      final typeValue = entityValue.toTypeValue();
      if (typeValue != null) {
        final entityElement = typeValue.element;
        if (entityElement is ClassElement && entityElement.name != null) {
          entityElements[entityElement.name!] = entityElement;

          // Extract table name from @Entity annotation
          String tableName = _toSnakeCase(entityElement.name!);
          for (final meta in entityElement.metadata.annotations) {
            if (meta.element?.enclosingElement?.name == 'Entity') {
              final source = meta.toSource();
              final tableMatch = RegExp(
                'tableName:\\s*[\'"]([\\w_]+)[\'"]',
              ).firstMatch(source);
              if (tableMatch != null) {
                tableName = tableMatch.group(1)!;
              }
              break;
            }
          }
          entityToTableName[entityElement.name!] = tableName;
        }
      }
    }

    // Second pass: extract OneToMany relationships first
    for (final entityValue in entityList) {
      final typeValue = entityValue.toTypeValue();
      if (typeValue != null) {
        final entityElement = typeValue.element;
        if (entityElement is ClassElement && entityElement.name != null) {
          final tableName = entityToTableName[entityElement.name!]!;

          // Extract OneToMany relationships
          final o2mRelations = _extractOneToManyRelations(
            entityElement,
            tableName,
            entityToTableName,
          );
          oneToManyRelations.addAll(o2mRelations);
        }
      }
    }

    // Third pass: extract entity info and relationships
    for (final entityValue in entityList) {
      final typeValue = entityValue.toTypeValue();
      if (typeValue != null) {
        final entityElement = typeValue.element;
        if (entityElement is ClassElement && entityElement.name != null) {
          final tableName = entityToTableName[entityElement.name!]!;

          // Extract fields/columns from entity
          final columns = _extractColumnsFromEntity(entityElement);

          // Extract ManyToOne relationships and add FK columns if not already present
          final manyToOneRelations = _extractManyToOneRelations(
            entityElement,
            tableName,
            entityToTableName,
          );

          // Add FK columns from ManyToOne relationships if not already in columns
          for (final m2oRel in manyToOneRelations) {
            final fkColumnExists = columns.any(
              (c) => c.name == m2oRel.foreignKeyColumn,
            );
            if (!fkColumnExists) {
              columns.add(
                _ColumnInfo(
                  name: m2oRel.foreignKeyColumn,
                  dartType: 'int?',
                  sqlType: 'INTEGER',
                  isNullable: m2oRel.nullable,
                  isPrimaryKey: false,
                ),
              );
            }
          }

          // Add FK columns from OneToMany relationships (where this entity is the target)
          for (final o2mRel in oneToManyRelations) {
            if (o2mRel.targetTableName == tableName) {
              final fkColumnExists = columns.any(
                (c) => c.name == o2mRel.foreignKeyColumn,
              );
              if (!fkColumnExists) {
                columns.add(
                  _ColumnInfo(
                    name: o2mRel.foreignKeyColumn,
                    dartType: 'int?',
                    sqlType: 'INTEGER',
                    isNullable: true,
                    isPrimaryKey: false,
                  ),
                );
              }
              // Also add to manyToOneRelations for FK constraint generation
              manyToOneRelations.add(
                _ManyToOneInfo(
                  ownerEntityName: o2mRel.targetEntityName,
                  ownerTableName: o2mRel.targetTableName,
                  targetEntityName: o2mRel.ownerEntityName,
                  targetTableName: o2mRel.ownerTableName,
                  fieldName: '',
                  foreignKeyColumn: o2mRel.foreignKeyColumn,
                  referencedColumn: o2mRel.referencedColumn,
                  nullable: true,
                  onDelete: o2mRel.onDelete,
                  onUpdate: o2mRel.onUpdate,
                ),
              );
            }
          }

          // Extract ManyToMany relationships
          final m2mRelations = _extractManyToManyRelations(
            entityElement,
            tableName,
            entityElements,
            entityToTableName,
          );
          manyToManyRelations.addAll(m2mRelations);

          entities.add(
            _EntityInfo(
              className: entityElement.name!,
              repositoryName: '${entityElement.name}Repository',
              tableName: tableName,
              columns: columns,
              manyToOneRelations: manyToOneRelations,
            ),
          );
        }
      }
    }
  }

  // Deduplicate junction tables (keep only owning side)
  final junctionTables = _deduplicateJunctionTables(
    manyToManyRelations,
    entityToTableName,
  );

  // Generate current schema as JSON
  final currentSchema = _schemaToJson(entities, junctionTables);
  final schemaHash = _generateSchemaHash(entities, junctionTables);

  // Migration history file path
  final migrationsFilePath = '$packageRoot.dorm/$dbName.migrations.json';

  // Load previous schema and migration history
  final previousSchema = await _loadPreviousSchema(schemaFilePath);
  final migrationHistory = await _loadMigrationHistory(migrationsFilePath);

  // Compare schemas to detect changes
  final schemaChanges = _compareSchemas(previousSchema, currentSchema);

  // Determine if we need a new migration
  final hasChanges = schemaChanges.isNotEmpty;
  final isInitial = previousSchema == null;

  // Update migration history if there are changes
  if (hasChanges && !isInitial) {
    await _addMigrationToHistory(
      migrationsFilePath,
      migrationHistory,
      migrationVersion,
      schemaChanges,
      schemaHash,
    );
  } else if (isInitial) {
    // Initialize migration history with version 1
    await _initializeMigrationHistory(
      migrationsFilePath,
      schemaHash,
    );
  }

  // Save current schema for next comparison
  await _saveSchema(schemaFilePath, currentSchema);

  // Reload migration history after updates
  final updatedHistory = await _loadMigrationHistory(migrationsFilePath);

  return _generateMigrationCode(
    className: className!,
    entities: entities,
    junctionTables: junctionTables,
    migrationVersion: migrationVersion,
    sqlDialect: sqlDialect,
    schemaHash: schemaHash,
    previousSchema: previousSchema,
    schemaChanges: schemaChanges,
    migrationHistory: updatedHistory,
  );
}