generate method

Future<void> generate()

Implementation

Future<void> generate() async {
  if (!config.generateClientManagers) {
    _logger.info(
      'FeedItemReferenceManager generation is disabled (linked to generateClientManagers flag). Skipping.',
    );
    return;
  }

  final managersDir = p.join(config.outputDirectory, 'managers');
  final fileName = 'feed_item_reference_manager.g.dart';
  final filePath = p.join(managersDir, fileName);

  final className = 'FeedItemReferenceManager';
  final providerName = 'feedItemReferenceManagerProvider';

  final buffer = StringBuffer();
  buffer.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND');
  buffer.writeln('// ignore_for_file: constant_identifier_names');
  buffer.writeln('');
  buffer.writeln("import 'package:sqlite_async/sqlite_async.dart';");
  if (config.useRiverpod) {
    buffer.writeln(
      "import 'package:flutter_riverpod/flutter_riverpod.dart';",
    );
    buffer.writeln("import '../database.dart';");
  }
  buffer.writeln('');

  buffer.writeln(r"""

/// Represents an item reference stored in a feed.
class FeedItemReference {
final String itemSourceTable;
final String itemSourceId;
final int displayOrder;

FeedItemReference({
  required this.itemSourceTable,
  required this.itemSourceId,
  required this.displayOrder,
});

factory FeedItemReference.fromMap(Map<String, dynamic> map) {
  return FeedItemReference(
    itemSourceTable: map['item_source_table'] as String,
    itemSourceId: map['item_source_id'] as String,
    displayOrder: map['display_order'] as int,
  );
}
}

class FeedItemReferenceManager {
final SqliteDatabase db;
static const String _tableName = 'feed_item_references';

FeedItemReferenceManager(this.db);

/// Clears all items for the given [feedKey] and inserts/updates the new [items]
/// ensuring their display_order is set according to their position in the list.
Future<void> setFeedItems({
  required String feedKey,
  required List<FeedItemReference> items,
}) async {
  await db.writeTransaction((tx) async {
    // It's often still useful to clear items not present in the new list.
    // If items can only be added/reordered but not removed by this operation,
    // you might reconsider this delete. For a full "set", this delete is common.
    await tx.execute('DELETE FROM $_tableName WHERE feed_key = ?', [feedKey]);
    for (int i = 0; i < items.length; i++) {
      final item = items[i];
      // UPSERT: Insert the item or update its display_order if it already exists
      // (though with the preceding DELETE, it will always be an INSERT here unless
      // items list has duplicates for the same item_source_id/table for this feedKey).
      await tx.execute(
        '''INSERT INTO $_tableName (feed_key, item_source_table, item_source_id, display_order)
        VALUES (?, ?, ?, ?)
        ON CONFLICT(feed_key, item_source_table, item_source_id) DO UPDATE SET
          display_order = excluded.display_order''',
        [feedKey, item.itemSourceTable, item.itemSourceId, i],
      );
    }
  });
  await db.get('SELECT 1');
}

/// Adds a list of items to the start of the specified feed.
/// All items in the feed (new and existing) will be renumbered sequentially starting from 0.
/// The `displayOrder` property of the [newItemsToAdd] is ignored.
Future<void> addItemsToStart({
  required String feedKey,
  required List<FeedItemReference> newItemsToAdd,
}) async {
  if (newItemsToAdd.isEmpty) {
    return;
  }

  await db.writeTransaction((tx) async {
    // 1. Get current items' identifiers, ordered by their current display_order.
    // We only need itemSourceTable and itemSourceId to identify them.
    final existingItemsRaw = await tx.execute(
      'SELECT item_source_table, item_source_id FROM $_tableName WHERE feed_key = ? ORDER BY display_order ASC',
      [feedKey],
    );
    final List<FeedItemReference> existingItems = existingItemsRaw
        .map(
          (row) => FeedItemReference(
            itemSourceTable: row['item_source_table'] as String,
            itemSourceId: row['item_source_id'] as String,
            displayOrder:
                0, // This displayOrder is a placeholder, it will be reassigned.
          ),
        )
        .toList();

    // 2. Create the new combined list.
    // New items come first. Existing items are added if not already included from newItemsToAdd
    // (this handles moving an existing item to the start).
    final List<FeedItemReference> combinedList = [];
    final Set<String> addedItemUniqueKeys =
        {}; // To track "table:id" to prevent duplicates

    for (final item in newItemsToAdd) {
      final uniqueKey = "${item.itemSourceTable}:${item.itemSourceId}";
      // We add all newItemsToAdd, their position at the start is guaranteed.
      // If a new item is a duplicate of another new item, both will be added here,
      // and the last one's position will be based on its order in newItemsToAdd.
      // The final INSERT loop will assign display_order based on this combinedList.
      combinedList.add(
        FeedItemReference(
          itemSourceTable: item.itemSourceTable,
          itemSourceId: item.itemSourceId,
          displayOrder: 0,
        ),
      ); // Placeholder displayOrder
      addedItemUniqueKeys.add(uniqueKey);
    }

    for (final item in existingItems) {
      final uniqueKey = "${item.itemSourceTable}:${item.itemSourceId}";
      if (!addedItemUniqueKeys.contains(uniqueKey)) {
        combinedList.add(
          FeedItemReference(
            itemSourceTable: item.itemSourceTable,
            itemSourceId: item.itemSourceId,
            displayOrder: 0,
          ),
        ); // Placeholder displayOrder
      }
    }

    // 3. Clear all existing items for this feedKey (within the transaction).
    await tx.execute('DELETE FROM $_tableName WHERE feed_key = ?', [feedKey]);

    // 4. Insert all items from the combined list with new, sequential display_order.
    // Since we've deleted all items for this feedKey and combinedList ensures uniqueness
    // of (itemSourceTable, itemSourceId) by how it's constructed, a simple INSERT is sufficient.
    // If combinedList could have internal duplicates, an UPSERT would be needed here.
    for (int i = 0; i < combinedList.length; i++) {
      final itemToInsert = combinedList[i];
      await tx.execute(
        '''INSERT INTO $_tableName (feed_key, item_source_table, item_source_id, display_order)
           VALUES (?, ?, ?, ?)''',
        [
          feedKey,
          itemToInsert.itemSourceTable,
          itemToInsert.itemSourceId,
          i, // New sequential display_order
        ],
      );
    }
  });
}

/// Adds a single item to the end of the specified feed.
/// If the item already exists in the feed, its display_order will be updated to move it to the end.
Future<void> addItemToEnd({
  required String feedKey,
  required String itemSourceTable,
  required String itemSourceId,
}) async {
  await db.writeTransaction((tx) async {
    final maxOrderResult = await tx.execute(
      'SELECT MAX(display_order) as max_order FROM $_tableName WHERE feed_key = ?',
      [feedKey],
    );
    final maxOrder = (maxOrderResult.isNotEmpty
            ? maxOrderResult.first['max_order'] as int?
            : null) ??
        -1;
    // UPSERT: Insert the item or update its display_order if it already exists.
    await tx.execute(
      '''INSERT INTO $_tableName (feed_key, item_source_table, item_source_id, display_order)
      VALUES (?, ?, ?, ?)
      ON CONFLICT(feed_key, item_source_table, item_source_id) DO UPDATE SET
        display_order = excluded.display_order''',
      [feedKey, itemSourceTable, itemSourceId, maxOrder + 1],
    );
  });
}

/// Adds a list of items to the end of the specified feed.
/// If an item already exists, its position is updated to the end.
/// This is more efficient than the previous implementation for pagination.
Future<void> addItemsToEnd({
  required String feedKey,
  required List<FeedItemReference> itemsToAdd,
}) async {
  if (itemsToAdd.isEmpty) {
    return;
  }

  await db.writeTransaction((tx) async {
    // 1. Get the current max display order for the feed.
    final maxOrderResult = await tx.execute(
      'SELECT MAX(display_order) as max_order FROM $_tableName WHERE feed_key = ?',
      [feedKey],
    );
    int maxOrder = (maxOrderResult.isNotEmpty
            ? maxOrderResult.first['max_order'] as int?
            : null) ??
        -1;

    // 2. Prepare a single UPSERT statement for all new items.
    final columns = [
      'feed_key',
      'item_source_table',
      'item_source_id',
      'display_order'
    ];
    final valuePlaceholderGroup = '(?, ?, ?, ?)';
    final allPlaceholders =
        List.filled(itemsToAdd.length, valuePlaceholderGroup).join(', ');
    final allArguments = <Object?>[];

    for (final item in itemsToAdd) {
      maxOrder++; // Increment order for each new item
      allArguments.addAll([
        feedKey,
        item.itemSourceTable,
        item.itemSourceId,
        maxOrder,
      ]);
    }

    final sql = '''
      INSERT INTO $_tableName (${columns.join(', ')})
      VALUES $allPlaceholders
      ON CONFLICT(feed_key, item_source_table, item_source_id) DO UPDATE SET
        display_order = excluded.display_order
    ''';

    await tx.execute(sql, allArguments);
  });
}

/// Removes a specific item from the feed. Note: This does NOT automatically re-compact display_order values.
Future<void> removeItem({
  required String feedKey,
  required String itemSourceTable,
  required String itemSourceId,
}) async {
  await db.execute(
    'DELETE FROM $_tableName WHERE feed_key = ? AND item_source_table = ? AND item_source_id = ?',
    [feedKey, itemSourceTable, itemSourceId],
  );
}

/// Removes an item by its source ID from all feeds.
/// This is useful when an item is permanently deleted and must be removed
/// from all feed references.
Future<void> removeItemFromAllFeeds({
  required String itemSourceId,
}) async {
  await db.execute(
    'DELETE FROM $_tableName WHERE item_source_id = ?',
    [itemSourceId],
  );
}

/// Removes all items from the specified feed.
Future<void> clearFeed({required String feedKey}) async {
  await db.execute('DELETE FROM $_tableName WHERE feed_key = ?', [feedKey]);
}

/// Gets the total count of items for a given feed.
Future<int> getCount({required String feedKey}) async {
  final result = await db.execute(
    'SELECT COUNT(*) as count FROM $_tableName WHERE feed_key = ?',
    [feedKey],
  );
  if (result.isNotEmpty) {
    return result.first['count'] as int? ?? 0;
  }
  return 0;
}

/// Retrieves all item references for a given feed, ordered by their display_order.
Future<List<FeedItemReference>> getFeedItemReferences({
  required String feedKey,
}) async {
  final results = await db.execute(
    'SELECT item_source_table, item_source_id, display_order FROM $_tableName WHERE feed_key = ? ORDER BY display_order ASC',
    [feedKey],
  );
  return results.map((map) => FeedItemReference.fromMap(map)).toList();
}

/// Reorders the items in a feed according to the provided list of source IDs.
/// This method updates the display_order of each item to match its index in the
/// provided [orderedSourceIds] list.
Future<void> reorderFeedItems({
  required String feedKey,
  required List<String> orderedSourceIds,
}) async {
  await db.writeTransaction((tx) async {
    for (int i = 0; i < orderedSourceIds.length; i++) {
      final id = orderedSourceIds[i];
      await tx.execute(
        'UPDATE feed_item_references SET display_order = ? WHERE feed_key = ? AND item_source_id = ?',
        [i, feedKey, id],
      );
    }
  });
}
}
""");

  buffer.writeln('');

  if (config.useRiverpod) {
    buffer.writeln(
      'final $providerName = Provider<$className>((',
    ); // Adjusted for multiline
    buffer.writeln('  ref,'); // Added ref for multiline
    buffer.writeln(') {');
    buffer.writeln(
      '  final database = ref.watch(databaseProvider);',
    );
    buffer.writeln('  return $className(database.db);');
    buffer.writeln('});');
  }

  try {
    final file = File(filePath);
    await file.parent.create(recursive: true);
    await file.writeAsString(buffer.toString());
    _logger.info('Generated feed item reference manager: $filePath');
  } catch (e) {
    _logger.severe(
      'Error writing feed item reference manager file $filePath: $e',
    );
  }
}