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 dbName = annotation.peek('name')?.stringValue;
  final generateSql = annotation.peek('generateSql')?.boolValue ?? false;
  final sqlDialectValue = annotation.peek('sqlDialect')?.objectValue;

  // 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;
  }

  // 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>{}; // Map entity name to table name

  // 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 (including ManyToOne FK columns)
          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,
            sqlDialect,
          );
          manyToManyRelations.addAll(m2mRelations);

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

  // Validate ManyToMany relationships
  final validationErrors = _validateManyToManyRelations(
    manyToManyRelations,
    entityElements,
  );
  if (validationErrors.isNotEmpty) {
    throw InvalidGenerationSourceError(
      'ManyToMany configuration errors:\n${validationErrors.join('\n')}',
      element: element,
    );
  }

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

  // Generate schema hash to detect changes (include junction tables)
  final schemaHash = _generateSchemaHash(entities, junctionTables);

  // Generate SQL file if requested
  if (generateSql) {
    await _generateSqlFile(
      buildStep: buildStep,
      dbName: dbName ?? className!.toLowerCase(),
      entities: entities,
      junctionTables: junctionTables,
      sqlDialect: sqlDialect,
      migrationVersion: migrationVersion,
    );
  }

  return _generateDbCode(
    className: className!,
    entities: entities,
    junctionTables: junctionTables,
    migrationVersion: migrationVersion,
    configInfo: configInfo,
    dbName: dbName,
    schemaHash: schemaHash,
  );
}