using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;
namespace Ban.Sdaid.Icd.Arch.Tech.CSharp
{
/// <summary>
/// ADR-SRV-002: Контракт grid-запросов (запрос/ответ). Поверх CQRS-обёртки (ADR-SRV-001)
/// фиксирует форму списочных запросов: пагинацию, сортировку, классы фильтров, форму ответа.
/// </summary>
public class ADR_SRV_002_GridQueryContract : IAdrDocument, IFolder<ArchCSharpFolder>
{
public string Name => "ADR-SRV-002. Контракт grid-запросов";
public string Description =>
@"Ряд экранов показывает табличные списки сущностей (очередь обработки, карты, документы). Базовый
CQRS (ADR-SRV-001) задаёт обёртку, но не контракт списка: как устроены пагинация и сортировка, как
именуются и применяются фильтры, что в ответе. Фиксируем единый контракт до первого реального grid-хендлера,
чтобы все списки строились по одному шаблону, а фронт имел один клиентский паттерн.";
public string Version => "1.0";
public string Status => "accepted";
public string[] Comments => new[]
{
"2026-08-12. Принято вместе с ADR-SRV-001. Первый список (очередь/карты) появится позже — контракт фиксируем заранее. Для справочников ввода (не список) не применяется.",
};
public Type? Supersedes => null;
public static string S1_Context = """
## Контекст и постановка задачи
UI будет показывать табличные представления сущностей (очередь обработки, информационные карты,
экземпляры фонда). Базовая CQRS-инфраструктура (ADR-SRV-001) даёт обёртку ответа, но не сам
**контракт списка** — какие поля у запроса и ответа, как именуются фильтры, как ведут себя
сортировка и пагинация.
Без единого контракта каждый список мог бы описывать страницы по-своему (0-based vs 1-based,
offset vs cursor), возвращать или не возвращать общее число, ждать имена колонок сортировки в
разных форматах, по-разному структурировать фильтры и по-разному сериализовать enum. Контракт
фиксируется **до** первого реального grid-хендлера.
> **Термины.** *Offset-пагинация* — сервер пропускает первые N записей (`SKIP/OFFSET`): просто,
> но дороже на больших объёмах. *Cursor-пагинация* — клиент передаёт «якорь» последней записи,
> сервер ищет после него: быстро по индексу, но нельзя прыгнуть на произвольную страницу.
""";
public static string S2_DecisionDrivers = """
## Драйверы решения
1. **Единообразие клиента** — фронт пишет один «строитель grid-запросов», а не логику в каждой фиче.
2. **Производительность БД** — избегаем `COUNT(*)` на больших таблицах.
3. **Безопасность сортировки** — имя колонки приходит с клиента; нужен whitelist, чтобы имя
идентификатора не стало вектором инъекции.
4. **Расширяемость без ломки** — multi-column sort и новые типы фильтров должны добавляться без
breaking change.
""";
public static string S3_ConsideredOptions = """
## Рассмотренные варианты
- **A (выбран)** — 1-based offset-пагинация + `HasMore` через N+1, типизированный `UserFilter`
отдельным record, whitelist имён сортировки.
- **B. Cursor-пагинация** — производительнее по индексу, но без прыжков на произвольную страницу
и сложнее для UI.
- **C. Всегда возвращать `TotalCount`** — привычно, но `COUNT(*)` на каждый запрос дорог на
крупных таблицах.
""";
public static string S4_DecisionOutcome = """
## Решение
Выбран **Вариант A**: нет дорогого `COUNT(*)`, произвольная навигация сохраняется, фильтры
самодокументируемы.
### 1. Запрос
Все grid-запросы наследуют базовый:
```csharp
public abstract record IcdBaseGridQuery<TResult> where TResult : IIcdQueryResult
{
public required PagingModel Paging { get; init; } // обязательна
public SortingModel? Sorting { get; init; } // опциональна
}
```
Конкретный запрос добавляет только типизированный `UserFilter?`.
**Пагинация (`PagingModel`):** `PageIndex` (int, **1-based**, default 1), `ItemsPerPage`
(int, default 20). Ограничения min/max — на усмотрение конкретного хендлера, не в базовом классе.
**Сортировка (`SortingModel?`):** `PropertyName` (string, имя property элемента списка,
без учёта регистра) + `Direction` (`SortDirection`: `Ascending`/`Descending`). Пока одна колонка;
путь расширения до `SortingModel[]?` — **без breaking change**, когда UI потребует вторичную
сортировку. Недопустимое `PropertyName` → ошибка 400 с перечнем допустимых полей. При
`Sorting == null` хендлер применяет дефолт (обычно — по убыванию даты создания; для справочников
допустим естественный порядок с явным обоснованием).
### 2. Классы фильтров
| Класс | Кто задаёт | В контракте | Пример |
|---|---|---|---|
| **User** | пользователь через UI | да, `UserFilter?` | поиск по имени, выбор статуса |
| **Context** | UI автоматически из контекста | да, отдельное поле запроса | текущая организация/сессия |
| **System** | хендлер по бизнес-правилам | нет, применяется внутри | ограничение по правам |
**User-фильтр** — отдельный record `{QueryName}UserFilter`, все поля **nullable** (применяются
независимо): строки — вхождение без учёта регистра; enum — как **строковый идентификатор (EID)**,
не число (разрешается через домен); `Guid` — точное совпадение по FK; диапазоны дат —
`DateOnlyRangeModel { Min?, Max? }`.
### 3. Ответ (без TotalCount)
```
QueryResult : IcdBaseGridQueryResult<TPayload, TItem>
└── Payload { GridItems: TItem[]; HasMore: bool }
```
`TotalCount` **не возвращаем** — устраняем `COUNT(*)`; UI работает в режиме «загрузить ещё» /
факт наличия следующей страницы. Нумерация «страница N из M» намеренно не поддерживается.
### 4. HasMore через N+1
```csharp
var items = await sorted
.ApplyPaging(pageIndex, itemsPerPage, addOneMoreItem: true) // запросить на 1 больше
.ToArrayAsync(ct);
GridItems = items.Take(itemsPerPage).ToArray();
HasMore = items.Length > itemsPerPage;
```
### 5. Whitelist-сортировка в хендлере
```csharp
private static readonly SortingApplier<TDbEntity> Sorting =
new SortingApplier<TDbEntity>()
.AddSortingOption(nameof(GridItem.Name), x => x.Name);
```
Ключ — `nameof` property элемента списка; значение — выражение на внутренней модели.
`SortingApplier` служит whitelist-защитой от произвольных имён колонок.
### 6. Даты в контракте
Дата в ответе — `DateYmdModel` (`Year/Month/Day`, сериализуется как ISO `YYYY-MM-DD`);
диапазонный фильтр — `DateOnlyRangeModel` с независимыми `Min?`/`Max?`. Совместимо с генератором
клиента и локалью фронта (ADR-UI-015).
""";
public static string S5_PositiveConsequences = """
## Положительные следствия
- Один клиентский паттерн на все списки.
- Нет `COUNT(*)` — меньше нагрузки на БД.
- Whitelist-сортировка защищает от произвольных идентификаторов.
- Тонкий запрос + самодокументируемый `UserFilter`.
""";
public static string S6_NegativeConsequences = """
## Отрицательные следствия и компромиссы
- Нет общего числа страниц — классическая пагинация «1 из N» недоступна (принимаем ради
производительности).
- Клиент должен знать имена property для сортировки — приходят через генерируемый клиент,
рассинхронизации нет.
- Переход на multi-column sort потребует одновременной правки базового класса и клиентов — путь
миграции (`SortingModel?` → `SortingModel[]?`) зафиксирован.
""";
public static string S7_Validation = """
## Проверка
- Первый grid-хендлер реализован строго по контракту.
- Ответ содержит `payload { gridItems, hasMore }`, без `totalCount`.
- Невалидное `Sorting.PropertyName` → 400 с перечнем допустимых полей.
- Enum в фильтрах передаются строковым идентификатором, не числом.
""";
public static string S8_OpenQuestions = """
## Открытые вопросы / отложено
- **Multi-column sort** — расширение до `SortingModel[]?` при первом кейсе вторичной сортировки.
- **Полнотекстовый поиск** — реализация `SearchString` (движок, индексы) — отдельным ADR при
первой фиче с ним.
- **Единый лимит `ItemsPerPage`** — если понадобится защита от выгрузки всей таблицы, вынести в
общий pipeline-шаг.
""";
public static string S99_Related = $"""
## Связанные артефакты
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_001_Cqrs)} — базовый CQRS и обёртка ответа.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_003_EntityAndEfMapping)} — внутренние модели, на которых строится список.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_015_I18n)} — форматы дат на фронте.
""";
}
}