Architecture First: как небольшой командой с ИИ сделать альтернативу Jira — Часть 22.
В предыдущей статье мы научили веб-приложение прикреплять к задачам картинки. Почти одновременно с вложениями у задач появились комментарии.
Краткое содержание
Как устроены комментарии к задачам:
- Комментарий — это текст до 4096 символов, автор, даты создания и изменения и список вложений.
- Комментарии видит любой, у кого есть доступ к задаче. Писать их может тот, у кого есть право изменять задачу.
- Редактировать и удалять комментарий может только его автор. Править чужие комментарии пока не может никто: такой потребности у пользователей ещё не было.
- Комментарии загружаются отдельным запросом в фоне, поэтому задача открывается сразу, даже если обсуждение длинное.
- Пагинация не считает общее количество комментариев: запрос берёт на одну строку больше, чем нужно, и по ней понимает, есть ли следующая страница.
- Создание, изменение и удаление комментариев попадают в историю изменений задачи.
Модель
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. Запрос берёт на одну запись больше, чем размер страницы, и так понимает, есть ли следующая.
- Те же вложения и та же история. Комментарии используют общую модель вложений и пишутся в общий аудит-лог.
Что дальше
Интерфейс комментариев, как и истории, делал Николай — фронтенд-разработчик, который к тому времени уже работал над проектом вместе с нами. О том, что поменялось в проекте с приходом второго человека, — следующая статья.