openapi: 3.0.3
info:
  title: workspaces-api
  description: >-
    HTTP API сервиса workspaces-api (репозиторий `pdm/workspaces-api`).
    Хранит рабочие области (workspaces), документы, состояния (states),
    динамические состояния и приложения (apps). Схема составлена по
    маршрутам `cmd/api/bootstrap.go` и структурам пакетов `workspace` и `apps`.

    Группы маршрутов:
      * публичные (`/ping`, `/metrics`) — без middleware;
      * `/api/v1` — внешний API, требует JWT (middleware `auth.JWTToCtx`);
      * `/internal/v1`, `/internal/v2` — внутренний API без JWT
        (доступ ограничивается сетевой политикой/ingress).
  version: "1.0.0"

servers:
  - url: https://api.sarex.io/workspaces
    description: production
  - url: https://api.preprod.sarex.io/workspaces
    description: preprod
  - url: https://stage-api.sarex.io/workspaces
    description: stage
  - url: http://localhost:6666
    description: local

tags:
  - name: health
    description: Liveness/readiness и метрики
  - name: workspaces
    description: Рабочие области
  - name: documents
    description: Документы рабочих областей
  - name: states
    description: Состояния рабочих областей
  - name: dynamic-states
    description: Динамические состояния
  - name: apps
    description: Приложения и их инстансы
  - name: internal
    description: Внутренние эндпоинты (без JWT)

security:
  - bearerAuth: []

paths:
  # ------------------------------------------------------------------ health
  /ping:
    get:
      tags: [health]
      summary: Liveness/readiness проба
      security: []
      responses:
        "200":
          description: Сервис жив

  /metrics:
    get:
      tags: [health]
      summary: Метрики Prometheus
      security: []
      responses:
        "200":
          description: Метрики в формате Prometheus
          content:
            text/plain:
              schema:
                type: string

  # -------------------------------------------------------------- api/v1 · ws
  /api/v1/workspaces:
    post:
      tags: [workspaces]
      summary: Создать рабочую область (v2)
      description: Создаёт рабочую область по списку id документов (`CreateV2Workspace`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateV2WorkspaceRequest"
      responses:
        "200":
          description: Созданная рабочая область
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{ws_id}:
    get:
      tags: [workspaces]
      summary: Получить рабочую область
      parameters:
        - $ref: "#/components/parameters/WsId"
        - name: archived
          in: query
          description: Включать архивные сущности
          schema: { type: integer, enum: [0, 1] }
        - name: state
          in: query
          description: uuid состояния; при указании в ответ добавляется последнее состояние
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Рабочая область
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{id}/archive:
    post:
      tags: [workspaces]
      summary: Архивировать рабочую область
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{id}/unarchive:
    post:
      tags: [workspaces]
      summary: Разархивировать рабочую область
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{id}/cache:
    delete:
      tags: [workspaces]
      summary: Очистить кеш бандлов рабочей области
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { description: Кеш очищен }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{ws_id}/states:
    get:
      tags: [states]
      summary: Список состояний рабочей области
      parameters:
        - $ref: "#/components/parameters/WsId"
        - name: expand
          in: query
          description: "`state_json` — вернуть полные данные состояний (иначе — краткий список)"
          schema: { type: string, enum: [state_json] }
      responses:
        "200":
          description: Состояния
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/State" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [states]
      summary: Создать состояние рабочей области
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateStateRequest" }
      responses:
        "200":
          description: Созданное состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/State" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{ws_id}/documents:
    post:
      tags: [documents]
      summary: Добавить документ в рабочую область
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddDocumentRequest" }
      responses:
        "200": { description: Документ добавлен }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{ws_id}/documents/{doc_id}:
    delete:
      tags: [documents]
      summary: Удалить документ из рабочей области
      parameters:
        - $ref: "#/components/parameters/WsId"
        - $ref: "#/components/parameters/DocId"
      responses:
        "200": { description: Документ удалён }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{ws_id}/dynamic_states:
    get:
      tags: [dynamic-states]
      summary: Список динамических состояний рабочей области
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      responses:
        "200":
          description: Динамические состояния
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/DynamicState" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [dynamic-states]
      summary: Создать динамическое состояние
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDynamicStateRequest" }
      responses:
        "200":
          description: Созданное динамическое состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DynamicState" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/workspaces/{workspace_id}/apps/{app_id}/instances:
    post:
      tags: [apps]
      summary: Создать инстанс приложения в рабочей области
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/AppId"
      responses:
        "200":
          description: Созданный инстанс
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AppInstance" }
        "400":
          description: Некорректный запрос или дубликат инстанса
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  # -------------------------------------------------------- api/v1 · documents
  /api/v1/documents/{doc_id}:
    patch:
      tags: [documents]
      summary: Обновить документ
      description: "Не реализовано в текущем коде — всегда возвращает 400 `Implement me`."
      parameters: [ { $ref: "#/components/parameters/DocId" } ]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }

  /api/v1/documents/{id}/archive:
    post:
      tags: [documents]
      summary: Архивировать документ
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/documents/{id}/unarchive:
    post:
      tags: [documents]
      summary: Разархивировать документ
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  # ----------------------------------------------------------- api/v1 · states
  /api/v1/states/{id}:
    get:
      tags: [states]
      summary: Получить состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200":
          description: Состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/State" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      tags: [states]
      summary: Изменить состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SetStateRequest" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      tags: [states]
      summary: Удалить (архивировать) состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/states/{id}/archive:
    post:
      tags: [states]
      summary: Архивировать состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/states/{id}/unarchive:
    post:
      tags: [states]
      summary: Разархивировать состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  # -------------------------------------------------- api/v1 · dynamic states
  /api/v1/dynamic_states/{id}:
    get:
      tags: [dynamic-states]
      summary: Получить динамическое состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200":
          description: Динамическое состояние
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DynamicState" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      tags: [dynamic-states]
      summary: Изменить динамическое состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SetDynamicStateRequest" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      tags: [dynamic-states]
      summary: Удалить динамическое состояние
      parameters: [ { $ref: "#/components/parameters/Id" } ]
      responses:
        "200": { description: Удалено }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  # ------------------------------------------------------------ api/v1 · apps
  /api/v1/company/{company_id}/apps:
    get:
      tags: [apps]
      summary: Приложения компании
      parameters: [ { $ref: "#/components/parameters/CompanyId" } ]
      responses:
        "200":
          description: Список приложений
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/App" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [apps]
      summary: Создать приложение
      parameters: [ { $ref: "#/components/parameters/CompanyId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AppAttributes" }
      responses:
        "200":
          description: Созданное приложение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/App" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/apps/{app_id}:
    patch:
      tags: [apps]
      summary: Изменить приложение
      parameters: [ { $ref: "#/components/parameters/AppId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChangeAppRequest" }
      responses:
        "200":
          description: Обновлённое приложение
          content:
            application/json:
              schema: { $ref: "#/components/schemas/App" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/apps/{app_id}/archive:
    post:
      tags: [apps]
      summary: Архивировать приложение
      parameters: [ { $ref: "#/components/parameters/AppId" } ]
      responses:
        "200": { description: Приложение архивировано }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/apps/{app_id}/unarchive:
    post:
      tags: [apps]
      summary: Разархивировать приложение
      parameters: [ { $ref: "#/components/parameters/AppId" } ]
      responses:
        "200": { description: Приложение разархивировано }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/app-instances/{instance_id}/archive:
    post:
      tags: [apps]
      summary: Архивировать инстанс приложения
      parameters: [ { $ref: "#/components/parameters/InstanceId" } ]
      responses:
        "200": { description: Инстанс архивирован }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/app-instances/{instance_id}/unarchive:
    post:
      tags: [apps]
      summary: Разархивировать инстанс приложения
      parameters: [ { $ref: "#/components/parameters/InstanceId" } ]
      responses:
        "200": { description: Инстанс разархивирован }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  # -------------------------------------------------------------- internal/v1
  /internal/v1/workspaces:
    post:
      tags: [internal]
      summary: Создать рабочую область (v1, по ссылкам на бандлы)
      description: Внутренний эндпоинт без JWT. Создаёт рабочую область по списку URL бандлов.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" }
      responses:
        "200":
          description: Созданная рабочая область
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /internal/v1/workspaces/{ws_id}/documents:
    post:
      tags: [internal]
      summary: Добавить документ в рабочую область (без проверки прав)
      security: []
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddDocumentRequest" }
      responses:
        "200": { description: Документ добавлен }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /internal/v1/workspaces/{ws_id}/states:
    get:
      tags: [internal]
      summary: Список состояний рабочей области (internal)
      security: []
      parameters: [ { $ref: "#/components/parameters/WsId" } ]
      responses:
        "200":
          description: Состояния
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/State" }
        "500": { $ref: "#/components/responses/InternalError" }

  # -------------------------------------------------------------- internal/v2
  /internal/v2/workspaces:
    post:
      tags: [internal]
      summary: Создать рабочую область (v2, internal)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateV2WorkspaceRequest" }
      responses:
        "200":
          description: Созданная рабочая область
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /internal/v2/workspaces/is_workspaces_v2:
    post:
      tags: [internal]
      summary: Проверить, являются ли рабочие области v2
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IsWorkspaceV2Request" }
      responses:
        "200":
          description: Карта id → признак v2
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IsWorkspaceV2Response" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

  /internal/v2/documents/{docs_id}:
    delete:
      tags: [internal]
      summary: Удалить документ (internal)
      security: []
      parameters:
        - name: docs_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Документ удалён }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /internal/v2/documents/restore:
    patch:
      tags: [internal]
      summary: Восстановить документы и рабочие области (internal)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RestoreDocumentsRequest" }
      responses:
        "200": { description: Восстановлено }
        "400": { $ref: "#/components/responses/BadRequest" }
        "500": { $ref: "#/components/responses/InternalError" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        JWT в заголовке `Authorization: Bearer <token>`. Токен также может
        передаваться query-параметром `jwt` (удаляется middleware после разбора).

  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WsId:
      name: ws_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WorkspaceId:
      name: workspace_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DocId:
      name: doc_id
      in: path
      required: true
      schema: { type: string }
    AppId:
      name: app_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    InstanceId:
      name: instance_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CompanyId:
      name: company_id
      in: path
      required: true
      schema: { type: integer, format: int64 }

  responses:
    Ok:
      description: Успех
      content:
        application/json:
          schema: { $ref: "#/components/schemas/OkResponse" }
    BadRequest:
      description: Некорректный запрос
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Ресурс не найден
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    InternalError:
      description: Внутренняя ошибка сервера
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    ErrorResponse:
      type: object
      properties:
        error: { type: string }
      required: [error]

    OkResponse:
      type: object
      properties:
        ok: { type: boolean }
      required: [ok]

    # ---- requests ----
    CreateWorkspaceRequest:
      type: object
      description: "`workspace/api/workspace/create.go` (v1)"
      properties:
        title: { type: string, maxLength: 254 }
        subtitle: { type: string, maxLength: 254 }
        target: { type: integer, nullable: true }
        files:
          type: array
          description: Ссылки на бандлы документов
          items: { type: string }
        features:
          type: array
          items: { type: string }
      required: [title, files]

    CreateV2WorkspaceRequest:
      type: object
      description: "`workspace/api/workspace/createV2.go` (v2)"
      properties:
        title: { type: string, maxLength: 254 }
        subtitle: { type: string, maxLength: 254 }
        target: { type: integer, nullable: true }
        documents:
          type: array
          description: id документов сервиса documentation
          items: { type: integer, format: int64 }
        features:
          type: array
          items: { type: string }
        auto_created: { type: boolean }
      required: [documents]

    IsWorkspaceV2Request:
      type: object
      properties:
        ids:
          type: array
          items: { type: string }
      required: [ids]

    IsWorkspaceV2Response:
      type: object
      properties:
        result:
          type: object
          additionalProperties: { type: boolean }
          description: Карта uuid рабочей области → является ли она v2

    AddDocumentRequest:
      type: object
      properties:
        document_id: { type: integer, format: int64 }
      required: [document_id]

    RestoreDocumentsRequest:
      type: object
      properties:
        document_ids:
          type: array
          minItems: 1
          items: { type: integer, format: int64 }
      required: [document_ids]

    CreateStateRequest:
      type: object
      properties:
        name: { type: string, maxLength: 254 }
        data: { type: string }
        dynamic_state_id: { type: string, format: uuid, nullable: true }
      required: [name, data]

    SetStateRequest:
      type: object
      description: Частичное обновление; все поля опциональны
      properties:
        name: { type: string, nullable: true }
        is_default: { type: boolean, nullable: true }
        data: { type: string, nullable: true }

    CreateDynamicStateRequest:
      type: object
      properties:
        name: { type: string, maxLength: 254 }
        autoload: { type: boolean }
      required: [name]

    SetDynamicStateRequest:
      type: object
      properties:
        name: { type: string, nullable: true }
        autoload: { type: boolean, nullable: true }

    AppAttributes:
      type: object
      description: "`apps/models.go`"
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        entrypoint: { type: string, minLength: 1, maxLength: 1024, format: uri }
        devices:
          type: array
          items: { type: string, enum: [mobile, tablet, desktop] }
        module_name: { type: string, minLength: 1, maxLength: 100 }
      required: [name, entrypoint, devices, module_name]

    ChangeAppRequest:
      type: object
      description: >-
        Частичное обновление приложения; должно быть задано хотя бы одно поле
        (`apps/api/changeapp.go`).
      properties:
        name: { type: string, maxLength: 100 }
        entrypoint: { type: string, maxLength: 1024, format: uri }
        devices:
          type: array
          items: { type: string, enum: [mobile, tablet, desktop] }
        module_name: { type: string, maxLength: 100 }

    # ---- models ----
    Workspace:
      type: object
      description: "Агрегат рабочей области (`workspace.Model`)"
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        subtitle: { type: string }
        features:
          type: array
          items: { type: string }
        target: { type: integer, nullable: true }
        auto_created: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        documents:
          type: array
          items: { $ref: "#/components/schemas/Document" }
        documents_v2:
          type: array
          items: { $ref: "#/components/schemas/DocumentV2" }
        state:
          allOf: [ { $ref: "#/components/schemas/State" } ]
          nullable: true
        app_instances:
          type: array
          items: { $ref: "#/components/schemas/AppInstance" }

    Document:
      type: object
      description: "`workspace.ModelDocument`"
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        workspace_id: { type: string, format: uuid }
        is_archived: { type: boolean }
        kind: { type: string }
        url: { type: string }
        bundle: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    DocumentV2:
      type: object
      description: "`workspace.ModelDocumentV2`"
      properties:
        id: { type: string, format: uuid }
        document_id: { type: integer, format: int64 }
        created_at: { type: string, format: date-time }

    State:
      type: object
      description: "`workspace.ModelState`"
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        workspace_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        data: { type: string }
        author_id: { type: integer, format: int64, nullable: true }
        is_default: { type: boolean }
        indestructible: { type: boolean }
        dynamic_state_id: { type: string, format: uuid, nullable: true }

    DynamicState:
      type: object
      description: "`workspace.ModelDynamicState`"
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        author_id: { type: integer, format: int64, nullable: true }
        autoload: { type: boolean }
        workspace_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        states:
          type: array
          items: { $ref: "#/components/schemas/State" }

    App:
      type: object
      description: "`apps.App`"
      properties:
        id: { type: string, format: uuid }
        company_id: { type: integer, format: int64 }
        name: { type: string }
        entrypoint: { type: string }
        devices:
          type: array
          items: { type: string, enum: [mobile, tablet, desktop] }
        module_name: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AppInstance:
      type: object
      description: "`apps.AppInstance`"
      properties:
        id: { type: string, format: uuid }
        app_id: { type: string, format: uuid }
        workspace_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        app:
          allOf: [ { $ref: "#/components/schemas/App" } ]
          nullable: true
