Назад к статьям
Статья

Комментарии к задачам — права, загрузка в фоне и пагинация API без COUNT

Architecture First: как небольшой командой с ИИ сделать альтернативу Jira — Часть 22.
В предыдущей статье мы научили веб-приложение прикреплять к задачам картинки. Почти одновременно с вложениями у задач появились комментарии.

Краткое содержание

Как устроены комментарии к задачам:

  1. Комментарий — это текст до 4096 символов, автор, даты создания и изменения и список вложений.
  2. Комментарии видит любой, у кого есть доступ к задаче. Писать их может тот, у кого есть право изменять задачу.
  3. Редактировать и удалять комментарий может только его автор. Править чужие комментарии пока не может никто: такой потребности у пользователей ещё не было.
  4. Комментарии загружаются отдельным запросом в фоне, поэтому задача открывается сразу, даже если обсуждение длинное.
  5. Пагинация не считает общее количество комментариев: запрос берёт на одну строку больше, чем нужно, и по ней понимает, есть ли следующая страница.
  6. Создание, изменение и удаление комментариев попадают в историю изменений задачи.

Модель

public class IssueComment
{
    public long Id { get; set; }

    [MaxLength(Constraints.MaxCommentLength)]
    public required string Text { get; set; }

    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    public Guid OwnerId { get; set; }
    public User? Owner { get; set; }

    public long IssueId { get; set; }
    public Issue? Issue { get; set; }

    public List<IssueCommentAttachment> Attachments { get; set; } = [];
}

Вложения комментариев используют ту же модель Attachment, что и вложения задач, — её мы разбирали в предыдущей статье. Комментарий связан с вложением через свою таблицу IssueCommentAttachment, а загрузка, проверка и хранение файлов работают так же, как для задач.

Кто может читать, писать и редактировать

Права на комментарии опираются на права на задачу, которые мы разбирали в части 17:

  • Читать комментарии может любой, у кого есть доступ к задаче.
  • Писать комментарий может тот, у кого есть право изменять задачу.
  • Редактировать и удалять комментарий может только его автор. Права на задачу здесь не важны: даже администратор организации не может поменять чужой комментарий.

Проверка в сервисе для изменения комментария простая:

if (comment?.OwnerId != request.AuthData.UserId)
    throw new ForbiddenException($"Comment: {request.CommentId} is not exists or not available to edit");

Возможность править чужие комментарии мы пока не добавляли: такой потребности у пользователей ещё не было.

Интерфейсу нужно знать, показывать ли у комментария кнопки «Изменить» и «Удалить». Поэтому признак CanModify вычисляется прямо в запросе, который загружает комментарии, — отдельно проверять права для каждого комментария не нужно.

Отдельный запрос и загрузка в фоне

Комментарии не входят в ответ, который возвращает задачу. У них свой запрос с пагинацией, и интерфейс отправляет его в фоне, когда задача уже открыта. Так задача открывается быстро, даже если под ней длинное обсуждение: пользователь уже видит задачу, пока загружаются комментарии.

Пагинация без COUNT

Обычная пагинация возвращает вместе со страницей общее количество записей, чтобы интерфейс мог показать «страница 3 из 12». Для этого базе нужен второй запрос — COUNT(*) по всем записям, которые подходят под условие.

Для комментариев общее количество не нужно: достаточно знать, есть ли следующая страница. Поэтому мы используем короткую пагинацию: запрос берёт на одну запись больше, чем размер страницы.

public static async Task<ShortPaginatedResult<TEntity>> ShortPaginateEFAsync<TEntity>(
    this IQueryable<TEntity> query,
    IPaginationData pagination,
    CancellationToken ct = default)
    where TEntity : class
{
    var skip = pagination.Page * pagination.PerPage;

    var data = await query.Skip(skip)
        .Take(pagination.PerPage + 1)
        .AsNoTracking()
        .ToListAsync(ct);

    return ShortPaginatedResultUtil.BuildResult(pagination, data);
}

Если вернулось больше записей, чем размер страницы, значит, следующая страница есть. Лишняя запись отбрасывается, а в ответе остаётся только признак HasNextPage:

var hasNextPage = data.Count > request.PerPage;
if (hasNextPage)
{
    data = data.Take(request.PerPage).ToList();
}

return new ShortPaginatedResult<TEntity>(request.Page, request.PerPage, hasNextPage, data);

Так страница загружается одним запросом вместо двух. Сейчас веб-приложение запрашивает первую страницу из 100 комментариев, а если в ответе HasNextPage = true, может загрузить следующую.

Оба вида пагинации — короткая и полная, которая дополнительно считает общее количество записей, — лежат в нашем пакете Laraue.Core (исходный код). Полную мы используем там, где это число видит пользователь, например для количества задач в колонке доски.

Комментарии одной задачи сортируются по Id, то есть в порядке создания, и страницы берутся через Skip/Take. Это offset-пагинация, а не курсорная: для комментариев одной задачи её достаточно.

Комментарии в истории

Создание, изменение и удаление комментария записываются в историю изменений так же, как изменения самой задачи: в ту же транзакцию, с типом сущности Comment. Поэтому в истории видно, кто и когда изменил комментарий и каким был текст до изменения.

Итоги

  • Читать может любой с доступом к задаче, писать — тот, кто может её изменять.
  • Редактировать и удалять — только автор. Чужие комментарии пока не может править никто: такой потребности ещё не было.
  • Отдельный запрос в фоне. Задача открывается сразу, а комментарии загружаются, пока пользователь её читает.
  • Пагинация без COUNT. Запрос берёт на одну запись больше, чем размер страницы, и так понимает, есть ли следующая.
  • Те же вложения и та же история. Комментарии используют общую модель вложений и пишутся в общий аудит-лог.

Что дальше

Интерфейс комментариев, как и истории, делал Николай — фронтенд-разработчик, который к тому времени уже работал над проектом вместе с нами. О том, что поменялось в проекте с приходом второго человека, — следующая статья.