⟨/⟩ 40_Arch/Tech/Angular/ADR_UI_012_ApiAndModels.cs

201 строк · в начало

using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;

namespace Ban.Sdaid.Icd.Arch.Tech.Angular
{
    /// <summary>
    /// ADR-UI-012: API-клиент и модели. Клиент генерируется из OpenAPI бэка и не пишется руками;
    /// UI не зависит от API-моделей напрямую — свои UI-модели + маппинг статической функцией в
    /// per-page service. Весь трафик к API — через сгенерированный клиент (включая multipart-загрузки).
    /// </summary>
    public class ADR_UI_012_ApiAndModels : IAdrDocument, IFolder<ArchAngularFolder>
    {
        public string Name => "ADR-UI-012. API-клиент и модели";

        public string Description =>
            @"UI ходит к бэку по REST. Контракт описан OpenAPI; клиент к нему **генерируется**, а не пишется
руками. Но UI не должен зависеть от API-моделей напрямую — иначе любая правка контракта расходится по
компонентам и шаблонам. Фиксируем: где живёт и как регистрируется сгенерированный клиент, как устроены
UI-модели, где и как выполняется маппинг API↔UI, и что весь трафик идёт через клиент.";

        public string Version => "1.0";
        public string Status => "accepted";
        public string[] Comments => new[]
        {
            "2026-08-13. Принято при переводе потребления API на изоляцию через генерируемый клиент; после того как бэк научили отдавать корректный multipart для загрузки файлов.",
        };

        public Type? Supersedes => null;

        public static string S1_Context = """
            ## Контекст и постановка задачи

            Фронт обращается к бэку по REST. Контракт описан OpenAPI-спекой, и клиент к нему **генерируется**
            (`swagger-codegen`, `src-ui/swagger-codegen/generate.py`), а не пишется вручную. Прямая зависимость
            UI от сгенерированных API-моделей опасна: переименование поля, смена формата даты, вложенные DTO —
            и правка расползается по компонентам и шаблонам.

            Нужно зафиксировать:

            - как генерируется, где лежит и как регистрируется клиент;
            - как организованы UI-модели и чем они отличаются от API-моделей;
            - где и как выполняется маппинг API ↔ UI;
            - что **всё** сетевое взаимодействие идёт через клиент (в т. ч. загрузка файлов).

            Контракт — **наш** (бэк в этом же репозитории), поэтому эндпоинты описываются так, чтобы генератор
            выдавал рабочие типизированные методы, а не подгоняются под ограничения инструмента задним числом.
            """;

        public static string S2_DecisionDrivers = """
            ## Драйверы решения

            1. **Изоляция UI от мутаций контракта.** Правка API — точечное изменение маппинга, не сквозная
               замена по шаблонам.
            2. **Минимум boilerplate.** Слой изоляции не должен плодить файлы ради самого себя — маппинг
               простой и локальный.
            3. **Прозрачность.** Понятно, где тип, где маппинг, где запрос.
            4. **Единый контракт ответа.** Все ручки возвращают одну обёртку (ADR-SRV-001) — фронт разворачивает
               её единообразно.
            5. **Совместимость с per-page паттерном** (ADR-UI-013): запрос+маппинг+эффекты — в сервисе страницы.
            """;

        public static string S3_ConsideredOptions = """
            ## Рассмотренные варианты

            - **A. UI работает прямо на API-моделях.** Минимум кода, максимум связности с контрактом — любая
              его правка бьёт по компонентам.
            - **B. Свои UI-модели; маппинг — всегда в отдельных mapper-файлах.** Чисто, но заводит слой-файл
              на каждый DTO даже там, где он не нужен.
            - **C. Свои UI-модели; маппинг — статической функцией в per-page service, отдельный
              `<feature>.mapper.ts` вводится только при росте/переиспользовании.** Локально, без лишних файлов.
            """;

        public static string S4_DecisionOutcome = """
            ## Решение

            Выбран **Вариант C**.

            ### Сгенерированный клиент

            - **Генерируется** из OpenAPI бэка (`src-ui/swagger-codegen/generate.py`), один контекст — `icd`.
            - **Место**: `src/app/services/api-clients/icd/`; импорт по алиасу `@api/icd` (ADR-UI-003).
            - **Не редактируется руками.** Любые правки — в бэке/контракте и перегенерации.
            - **Регистрация в DI — явная.** Сгенерированный `ApiModule` не используем (приложение
              standalone) — нужные api-сервисы перечисляются в `providers` `app.config.ts`. Базовый URL —
              токен `BASE_PATH` из `RuntimeConfigService` (ADR-UI-014).

            **Чеклист после перегенерации:** просмотреть `api-clients/icd/api/api.ts` (список сервисов) и
            добавить в `providers` `app.config.ts` каждый api-сервис, используемый в per-page сервисах.
            Пропуск даёт `NG0201: No provider for _XxxService` при первом обращении.

            ### UI-модели

            - **Общие** (не привязанные к фиче) — в `src/app/models/`: `Pagination`, `Sorting`, `Option<T>` и т. п.
            - **Доменные** — в `modules/<feature>/models/<entity>.ts` (имя типа — PascalCase сущности, без
              инфраструктурного префикса; имена файлов — по ADR-UI-002).

            UI-модель — это **то, что удобно показывать и редактировать**. Типичные отличия от API-модели:
            денормализация (плоские lookup-карты вместо вложенных ссылок), удобные форматы (enum-код → подпись,
            ISO-строка → отображаемая дата), поля «только для UI». Покрывать **все** поля API-модели она не обязана —
            только используемые страницей. Пример — справочники ввода (`modules/input/models/input-reference.ts`),
            наполняемые из `@api/icd` маппером.

            ### Маппинг API ↔ UI

            - **База** — статическая функция `toUi` в per-page service: `api.method(...).pipe(map(res => toUi(res.payload)))`.
            - **Вынос в `modules/<feature>/<feature>.mapper.ts`** — когда функция разрослась (≈>30–40 строк)
              или одна сущность маппится в нескольких сервисах фичи (переиспользование). Пример уже вынесенного —
              `modules/input/models/input-reference.mapper.ts`.
            - **Направления**: `toUi` (`ApiX → X`, чтение) и `toApi` (`X → команда`, запись; часто отдельные
              функции под create/update — состав полей команд различается).
            - **Шаблоны видят только UI-модели.** Импорт `@api/*` в шаблонах/компонентах-представлениях запрещён.

            ### Обёртка ответа

            Все ручки возвращают единый контракт `IcdBaseRequestResult` (ADR-SRV-001): `payload` / `isSuccess`
            / `errors[]`. Per-page service разворачивает `payload`, проверяет `isSuccess`, а при ошибке
            показывает `errors[].errorMessage` — тексты формирует бэк по-русски, UI их **не переводит**
            (ADR-UI-015).

            ### Весь трафик — через слой `api-clients`

            Per-page service **инжектирует сгенерированный api-сервис и зовёт его методы**; он не строит
            URL/`HttpClient`/`FormData` сам. Это касается и **загрузки файлов**: так как контракт наш,
            эндпоинт описывается корректным `multipart/form-data` (файлы — бинарные поля), и генератор выдаёт
            типизированный метод (напр. `apiInputPacketPostForm(payload, files)`), внутри которого сам собирает
            `FormData`. Корректность multipart-описания на стороне бэка обеспечивается отдельно (см. связанный
            backend-артефакт).

            ### Загрузка данных

            - Методы per-page service возвращают `Observable<UI-модель>` / `Observable<UI-модель[]>`; API-модели
              за пределы сервиса не выходят.
            - Компонент подписывается через `takeUntilDestroyed()` и кладёт результат в сигнал (ADR-UI-008)
              либо использует `toSignal()`.

            ### Чего не делаем

            - **Не используем API-модели в шаблонах.**
            - **Не правим сгенерированный клиент руками** — только перегенерация.
            - **Не тащим прямой `HttpClient`/`FormData`/`apiBaseUrl` в per-page service** (текущий обход на
              загрузке пакета — миграционный долг, S8).
            - **Не делаем универсальный авто-маппер** — явные функции дешевле в поддержке.
            - **Не кэшируем** данные между страницами (при появлении проблемы — отдельный ADR).
            """;

        public static string S5_PositiveConsequences = """
            ## Положительные следствия

            - Правка API-контракта — точечное изменение функции маппинга, не сквозная по компонентам.
            - UI-модели читаются как «то, что на экране», без шума серверных DTO.
            - Per-page service — единая точка запроса, маппинга и эффектов одной страницы.
            - Один контракт ответа и один способ разворачивать `payload`/`errors` на всех страницах.
            """;

        public static string S6_NegativeConsequences = """
            ## Отрицательные следствия и компромиссы

            - Дублирование структуры: API-модель + UI-модель + функция маппинга. Для тривиальных сущностей
              избыточно — принимаем как цену изоляции.
            - При множестве маппингов в одной фиче сервис разрастается — мониторим и выносим в `<feature>.mapper.ts`.
            - Требование «контракт под генератор» иногда обязывает поправить описание эндпоинта на бэке (напр.
              корректный multipart), а не обходить его на фронте.
            """;

        public static string S7_Validation = """
            ## Проверка

            - В шаблонах нет импортов из `@api/*`; шаблоны ссылаются только на UI-модели.
            - Per-page services возвращают типы из `modules/<feature>/models/` или `app/models/`, не из `@api/*`.
            - Каждый инжектируемый api-сервис перечислен в `providers` `app.config.ts` — открытие страницы не даёт `NG0201`.
            - Переименование поля в контракте → перегенерация клиента → правка одной функции маппинга → код собирается.
            - Загрузка файлов идёт через сгенерированный метод клиента, а не через прямой `HttpClient`+`FormData`.
            """;

        public static string S8_OpenQuestions = """
            ## Открытые вопросы / отложено

            - **Миграционный долг — сдача пакета.** Сейчас `modules/input/services/data-input-page.service.ts`
              шлёт `POST /api/InputPacket` напрямую `HttpClient`+`FormData` (историческое следствие некорректного
              multipart-описания на бэке). Бэк исправлен — перегенерировать клиент, зарегистрировать
              `InputPacketService` в `app.config.ts` и перевести сервис на `apiInputPacketPostForm(payload, files)`.
            - **Auth-ошибки (401/403).** Глобальная обработка interceptor-ом появится с auth-ADR; до тех пор
              ошибки ловит сам per-page service.
            - **Помощник `serverErrors → поля формы`** (ADR-UI-009) — введём при первой форме с серверной валидацией.
            - **Кэширование частых справочников** — отложено (пока каждая страница грузит заново).
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_003_ProjectStructure)} — место `api-clients/`, алиас `@api`, папки моделей.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_013_FeaturePagePattern)} — per-page service как точка запроса и маппинга.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_014_RuntimeConfig)} — базовый URL клиента через токен `BASE_PATH` из `RuntimeConfigService`.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_008_State)} — данные из API кладутся в сигналы.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_015_I18n)} — тексты ошибок формирует бэк, UI не переводит.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_001_Cqrs)} — единая обёртка ответа `IcdBaseRequestResult`.
            """;
    }
}