openapi: 3.0.3

info:
  title: Notes Service API
  version: "0.0.1"
  description: |
    REST API сервиса **notes-backend** (`aero/notes-backend`) — управление
    заметками к сущностям (workspace), их ссылками, документами и вложениями,
    а также генерацией документов через workflow.

    Сервис написан на Python (**FastAPI**, pydantic v1). Приложение собирается
    фабрикой `get_app` в `src/app/main.py`. Публичный роутинг подключается с
    префиксом `/api/v1` (`src/app/routers/__init__.py`) и включает группы
    `notes`, `links`, `documents`, `attachments`.

    Опционально (при `ENABLE_ND=true`) подключается роутер `nd_service` с
    префиксом `/api/v1/nd` (`src/app/nd_service/router.py`).

    ### Аутентификация
    Аутентификация выполняется middleware `DjangoUserMiddleware`
    (`src/app/middleware.py`). При `DJANGO_USE=true` каждый запрос (кроме путей,
    содержащих `/nd`) должен содержать заголовок `Authorization` — токен
    проверяется обращением к Django `/client/settings/`. Опционально
    передаётся заголовок `Identity` (режим Zitadel). Если заголовок
    `Authorization` отсутствует — возвращается `401`.

    При `DJANGO_USE=false` middleware подставляет тестового пользователя-
    администратора (использовать только локально).

    Дополнительно, права проверяются зависимостью `PermissionManager`:
    для не-админов метод сопоставляется с правом (`base.can_add_note` для POST,
    `base.can_view_note` для GET, `base.can_change_note` для PUT/PATCH,
    `base.can_delete_note` для DELETE); при отсутствии права — `403`.

    Часть эндпоинтов НД (`/api/v1/nd/nd_proxy/*` на запись) защищена
    JWT (`JWTBearer`, включается флагом `ND_JWT_ENABLE`).

    ### Замечания (расхождения кода)
    - Ошибки валидации тела/параметров (Pydantic) отдаются FastAPI в
      стандартном формате `422`.
    - Подключение к БД всегда идёт с `sslmode=verify-full` независимо от
      значения `PG_SSL_MODE`.
    - Многие пути завершаются слэшем (`/api/v1/notes/{id}/`).

  contact:
    name: notes-backend
    url: https://gitlab/aero/notes-backend

servers:
  - url: https://api.sarex.io/notes
    description: Production (ingress, BASE_HOST)
  - url: https://api.preprod.sarex.io/notes
    description: Preprod (ingress, BASE_HOST)
  - url: https://stage-api.sarex.io/notes
    description: Stage (ingress, BASE_HOST)
  - url: http://notes-backend-service:8000
    description: Внутрикластерный адрес (ClusterIP)
  - url: http://localhost:8000
    description: Локальный запуск (gunicorn/docker, порт 8000)

tags:
  - name: notes
    description: Заметки — создание, просмотр, обновление, поиск, документы и вложения
  - name: links
    description: Ссылки, привязанные к заметкам
  - name: documents
    description: Документы, привязанные к заметкам
  - name: attachments
    description: Вложения заметок
  - name: nd_service
    description: Проксирование НД (подключается при ENABLE_ND=true)

security:
  - bearerAuth: []

paths:
  /api/v1/notes/:
    post:
      tags: [notes]
      summary: Создать заметку
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BaseNote'
      responses:
        '201':
          description: Создано
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Note'
        '400':
          description: Некорректный company_id
    get:
      tags: [notes]
      summary: Список заметок
      parameters:
        - { name: search, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 1000, minimum: 0 } }
        - { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } }
        - { name: created_from, in: query, schema: { type: string, format: date-time } }
        - { name: created_to, in: query, schema: { type: string, format: date-time } }
        - { name: time_start, in: query, schema: { type: string, format: date-time } }
        - { name: time_end, in: query, schema: { type: string, format: date-time } }
        - { name: author, in: query, schema: { type: string } }
        - { name: description, in: query, schema: { type: boolean } }
        - { name: document, in: query, schema: { type: boolean } }
        - { name: resource_id, in: query, description: "Список UUID через запятую", schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Note'

  /api/v1/notes/{service}/{entity}/{instance_id}/:
    get:
      tags: [notes]
      summary: Заметки по сервису/сущности/инстансу
      parameters:
        - { name: service, in: path, required: true, schema: { $ref: '#/components/schemas/Service' } }
        - { name: entity, in: path, required: true, schema: { $ref: '#/components/schemas/Entity' } }
        - { name: instance_id, in: path, required: true, schema: { type: string } }
        - { name: full, in: query, description: "Вернуть расширенные заметки (ExtendedNote)", schema: { type: boolean, default: false } }
        - { name: search, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 1000 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  oneOf:
                    - $ref: '#/components/schemas/Note'
                    - $ref: '#/components/schemas/ExtendedNote'

  /api/v1/notes/{instance_id}/:
    get:
      tags: [notes]
      summary: Заметка по id
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Note' }
        '404':
          description: Не найдено
    put:
      tags: [notes]
      summary: Обновить заметку
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BaseNote' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Note' }
        '400': { description: Некорректный company_id }
        '404': { description: Не найдено }
    delete:
      tags: [notes]
      summary: Удалить заметку
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Удалено (возвращает удалённый объект)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Note' }
        '404': { description: Не найдено }

  /api/v1/notes/{instance_id}/documents/:
    post:
      tags: [notes]
      summary: Добавить документы к заметке
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items: { $ref: '#/components/schemas/BaseDocument' }
      responses:
        '201':
          description: Создано
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Document' }
        '404': { description: Заметка не найдена }

  /api/v1/notes/{instance_id}/attachments/:
    post:
      tags: [notes]
      summary: Загрузить файлы-вложения к заметке
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items: { type: string, format: binary }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    name: { type: string }
                    link: { type: string }
                    id: { type: integer }
                    attachment_type: { type: string }
        '404': { description: Заметка не найдена }
    get:
      tags: [notes]
      summary: Вложения заметки
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { type: object } }
        '404': { description: Заметка не найдена }

  /api/v1/notes/{instance_id}/bound_attachments/:
    post:
      tags: [notes]
      summary: Привязать существующее вложение к заметке
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BaseAttachment' }
      responses:
        '201':
          description: Создано
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Attachment' }
        '404': { description: Заметка не найдена }

  /api/v1/notes/{instance_id}/generate_document/:
    post:
      tags: [notes]
      summary: Сгенерировать документ по заметке (через workflow)
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DocumentCreation' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WFResponse' }
        '404': { description: Заметка не найдена }

  /api/v1/links/:
    post:
      tags: [links]
      summary: Создать ссылку
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BaseLink' }
      responses:
        '201':
          description: Создано
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Link' }
        '400': { description: Некорректный note_id }
    get:
      tags: [links]
      summary: Список ссылок
      parameters:
        - { name: note, in: query, description: "Фильтр по note_id", schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, default: 1000 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Link' } }

  /api/v1/links/{instance_id}/:
    get:
      tags: [links]
      summary: Ссылка по id
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Link' }
        '404': { description: Не найдено }
    put:
      tags: [links]
      summary: Обновить ссылку
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BaseLink' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Link' }
        '404': { description: Не найдено }
    delete:
      tags: [links]
      summary: Удалить ссылку
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Удалено
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Link' }
        '404': { description: Не найдено }

  /api/v1/documents/{instance_id}/:
    delete:
      tags: [documents]
      summary: Удалить документ заметки
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Удалено
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '404': { description: Не найдено }

  /api/v1/attachments/{instance_id}/:
    delete:
      tags: [attachments]
      summary: Удалить вложение
      parameters:
        - { name: instance_id, in: path, required: true, description: "attachment_id", schema: { type: integer } }
      responses:
        '200':
          description: Удалено
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Attachment' }
        '404': { description: Не найдено }

  /api/v1/nd/nd_proxy/:
    post:
      tags: [nd_service]
      summary: Создать НД-прокси
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NDProxyCreate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NDProxySchema' }
        '400': { description: Нарушение целостности }
    get:
      tags: [nd_service]
      summary: Список НД-прокси
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: is_bound, in: query, schema: { type: boolean } }
        - { name: limit, in: query, schema: { type: integer, default: 1000 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/NDProxySchema' } }

  /api/v1/nd/nd_proxy/{instance_id}/:
    get:
      tags: [nd_service]
      summary: НД-прокси по коду
      parameters:
        - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NDProxySchema' }
        '404': { description: Не найдено }
    put:
      tags: [nd_service]
      summary: Обновить НД-прокси
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NDProxyCreate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NDProxySchema' }
        '404': { description: Не найдено }
    patch:
      tags: [nd_service]
      summary: Частично обновить НД-прокси
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NDProxyUpdate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NDProxySchema' }
        '404': { description: Не найдено }
    delete:
      tags: [nd_service]
      summary: Удалить НД-прокси
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Удалено
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NDProxySchema' }
        '404': { description: Не найдено }

  /api/v1/nd/nd_proxy/{instance_id}/bound/:
    post:
      tags: [nd_service]
      summary: Привязать заметку и файл к НД-прокси
      parameters:
        - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                note_id: { type: integer }
                name: { type: string }
                file: { type: string, format: binary }
      responses:
        '200':
          description: OK
        '404': { description: Не найдено }

  /api/v1/nd/notes/{instance_id}/:
    get:
      tags: [nd_service]
      summary: Заметка НД с изображением
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: object }
        '404': { description: Не найдено }
    patch:
      tags: [nd_service]
      summary: Обновить время заметки (НД)
      parameters:
        - { name: instance_id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NDUpdateTimeNote' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Note' }
        '404': { description: Не найдено }

  /api/v1/nd/users/:
    post:
      tags: [nd_service]
      summary: Проверить существование пользователя по username
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/User' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  exist: { type: boolean }
        '400': { description: Ошибка запроса к Django }

  /api/v1/nd/token/:
    get:
      tags: [nd_service]
      summary: Сгенерировать JWT для НД-сервиса
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Заголовок `Authorization` (проверяется через Django `/client/settings/`).
        Опционально заголовок `Identity` для режима Zitadel. Эндпоинты `/api/v1/nd/*`
        на запись используют собственный JWT (`JWTBearer`, флаг ND_JWT_ENABLE).

  schemas:
    Service:
      type: string
      enum: [workspace]
    Entity:
      type: string
      enum: [workspace]

    BaseNote:
      type: object
      required: [name, service, entity, instance_id, company_id]
      properties:
        name: { type: string }
        body: { type: string, nullable: true }
        time_start: { type: string, format: date-time, nullable: true }
        time_end: { type: string, format: date-time, nullable: true }
        service: { $ref: '#/components/schemas/Service' }
        entity: { $ref: '#/components/schemas/Entity' }
        instance_id: { type: string }
        meta_data: { type: object, nullable: true }
        height_base_plane: { type: number, format: float, nullable: true }
        height_geom_prmtv: { type: number, format: float, nullable: true }
        created_by: { type: integer, nullable: true, minimum: 1 }
        company_id: { type: integer, minimum: 1 }
        resource_id: { type: string, format: uuid, nullable: true }

    Note:
      allOf:
        - $ref: '#/components/schemas/BaseNote'
        - type: object
          required: [id, created_at, updated_at]
          properties:
            id: { type: integer }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    ExtendedNote:
      allOf:
        - $ref: '#/components/schemas/Note'
        - type: object
          properties:
            links:
              type: array
              items: { type: integer }
            documents:
              type: array
              items: { $ref: '#/components/schemas/Document' }

    DocumentCreation:
      type: object
      required: [path, values, file_name, template]
      properties:
        path: { type: string }
        values: { type: object, additionalProperties: true }
        file_name: { type: string }
        template: { type: string }

    WFResponse:
      type: object
      required: [context]
      properties:
        context: { type: object }

    BaseLink:
      type: object
      required: [link_id, note_id]
      properties:
        link_id: { type: integer, minimum: 1 }
        note_id: { type: integer, minimum: 1 }

    Link:
      allOf:
        - $ref: '#/components/schemas/BaseLink'
        - type: object
          required: [id]
          properties:
            id: { type: integer }

    BaseDocument:
      type: object
      required: [document_id, bundle_id]
      properties:
        document_id: { type: integer }
        bundle_id: { type: string }
        note_id: { type: integer, nullable: true }

    Document:
      allOf:
        - $ref: '#/components/schemas/BaseDocument'
        - type: object
          required: [id]
          properties:
            id: { type: integer }

    BaseAttachment:
      type: object
      required: [attachment_id, object_name, attachment_type, is_state]
      properties:
        attachment_id: { type: integer, minimum: 1 }
        note_id: { type: integer, nullable: true, minimum: 1 }
        object_name: { type: string }
        attachment_type: { type: string }
        is_state: { type: boolean }

    Attachment:
      allOf:
        - $ref: '#/components/schemas/BaseAttachment'
        - type: object
          required: [id]
          properties:
            id: { type: integer }

    NDProxyCreate:
      type: object
      required: [nd_code, creator, is_locked]
      properties:
        nd_code: { type: string }
        work_description: { type: string, nullable: true }
        equipment_description: { type: string, nullable: true }
        description: { type: string, nullable: true }
        creator: { type: string }
        is_locked: { type: boolean }
        note_id: { type: integer, nullable: true }

    NDProxyUpdate:
      type: object
      properties:
        nd_code: { type: string, nullable: true }
        work_description: { type: string, nullable: true }
        equipment_description: { type: string, nullable: true }
        description: { type: string, nullable: true }
        creator: { type: string, nullable: true }
        is_locked: { type: boolean, nullable: true }
        note_id: { type: integer, nullable: true }

    NDProxySchema:
      allOf:
        - $ref: '#/components/schemas/NDProxyCreate'
        - type: object
          required: [id]
          properties:
            id: { type: integer }

    NDUpdateTimeNote:
      type: object
      properties:
        time_start: { type: string, format: date-time, nullable: true }
        time_end: { type: string, format: date-time, nullable: true }

    User:
      type: object
      required: [username]
      properties:
        username: { type: string }
