Add example .env file, configuration documentation, and OpenAPI specifications for stamp-verification service.

This commit is contained in:
emelinda 2026-07-13 21:40:40 +03:00
parent b4568003a3
commit bd9e42f43d
5 changed files with 1393 additions and 0 deletions

View File

@ -0,0 +1,70 @@
# =============================================================================
# stamp-verification-frontend — пример переменных окружения
# =============================================================================
#
# ВАЖНО: само приложение (статическая страница на nginx) НЕ читает переменные
# окружения во время выполнения. Конфигурация «зашита» в код:
# - базовый URL API выбирается по window.location.host (см. static/js/index.js);
# - nginx слушает порт 8080 (см. nginx.conf);
# - health-check отдаётся статически по пути /ping.
#
# Поэтому файл .env НЕ подхватывается образом. Перечисленные ниже переменные
# участвуют только в СБОРКЕ и ДЕПЛОЕ (GitLab CI + Helm/universal-chart) и
# приведены здесь как справочный шаблон значений для каждого окружения.
# -----------------------------------------------------------------------------
# --- GitLab CI (.gitlab-ci.yml) ---------------------------------------------
# Задаются пайплайном/переменными проекта, переключаются по ветке/тегу.
SERVICE_NAME=stamp-verification-frontend
DOCKERFILE_PATH=Dockerfile
BUILD_ARGS=
CI_TRIGGER_SOURCE=app
# STAND / NAMESPACE выставляются автоматически по правилам workflow:
# ветка stage -> STAND=stage NAMESPACE=documentations
# ветка master -> STAND=preprod NAMESPACE=stamp-verification-preprod
# тег (release) -> STAND=production NAMESPACE=stamp-verification-prod
STAND=stage
NAMESPACE=documentations
RELEASE_NAME=stamp-verification-frontend
CHART_NAME=stamp-verification-frontend
CHART_VERSION=0.0.1-stage
K8S_HUSTLER_BRANCH=universal-chart-stage
# Имя собранного образа (подставляется в HELM_SET_ARGS как
# universal-chart.services.frontend.image.name.<env>)
IMAGE_NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest
# --- Helm / universal-chart (.helm/values.yaml) -----------------------------
# Значения чарта universal-chart (dependency 0.1.7). env выбирает набор *.env.
UNIVERSAL_CHART__GLOBAL__ENV=stage # _default | stage | preprod | production
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__NAME=stamp-verification-frontend
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__PORT=8080
UNIVERSAL_CHART__SERVICES__FRONTEND__DEPLOYMENT__REPLICA_COUNT=1
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__NAME=cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE__PULL_POLICY=IfNotPresent
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__NAME=frontend-service
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__PORT=8080 # production: 80
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TARGET_PORT=8080
UNIVERSAL_CHART__SERVICES__FRONTEND__SERVICE__TYPE=ClusterIP
UNIVERSAL_CHART__SERVICES__FRONTEND__IMAGE_PULL_SECRETS__NAME=dockerhub
# --- Docker build (Dockerfile) ----------------------------------------------
# Базовый образ фиксирован в Dockerfile: nginx:mainline-alpine-otel.
# BUILD_ARGS пуст — build-time аргументов у образа нет.
# --- «Runtime»-конфигурация приложения (справочно, НЕ через env) -------------
# Значения ниже НЕ настраиваются переменными окружения — они захардкожены в
# static/js/index.js и приведены только для понимания поведения:
#
# window.location.host == "stamp-verification.sarex.io" -> API = api.sarex.io (prod)
# иначе (напр. stamp-verification.stage.sarex.io) -> API = stage-api.sarex.io (stage)
#
# ВНИМАНИЕ (безопасность): в static/js/index.js в заголовок Authorization
# захардкожен Bearer-JWT (истёк в 2023 г.). Это должно быть удалено —
# публичный эндпоинт QR не требует пользовательского токена. Секрета для env
# здесь нет и быть не должно.

View File

@ -0,0 +1,132 @@
# Конфигурация проекта stamp-verification-frontend
Документ описывает, как конфигурируется и разворачивается модуль **stamp-verification-frontend** — публичная статическая страница проверки штампа (QR) на документе.
## Что это за сервис
`stamp-verification-frontend` — это статический фронтенд (обычный HTML + ванильный JavaScript + `moment.js`), который раздаётся через **nginx**. По QR-коду со штампа на PDF пользователь попадает на страницу с параметрами `?id=<qrId>&page_number=<n>`, страница запрашивает данные документа у backend-сервиса `documentations` и отображает информацию о версии, авторе, дате загрузки, статусе согласования и ссылке на документ в Sarex.
Ключевое отличие от backend-сервисов: **у приложения нет разбора переменных окружения**. Здесь нет `pydantic-settings`, нет `.env`, нет секций конфигурации. Всё «runtime»-поведение либо статично, либо определяется по хосту в браузере (`window.location.host`). Конфигурируется только **сборка и деплой** (Docker, Helm/`universal-chart`, GitLab CI).
## Способы конфигурирования
| Слой | Где задаётся | Что настраивает |
| --- | --- | --- |
| Runtime (браузер) | `static/js/index.js` | Выбор базового URL API по `window.location.host`; параметры запроса берутся из query-строки (`id`, `page_number`, `v`) |
| Веб-сервер | `nginx.conf` | Порт прослушивания `8080`, корень `/dist`, health-check `GET /ping``200 {"result": "ok"}`, gzip, логи в stdout/stderr |
| Образ | `Dockerfile` | Базовый образ `nginx:mainline-alpine-otel`, копирование `static/` и `index.html` в `/dist`, `EXPOSE 8080` |
| Helm | `.helm/values.yaml`, `.helm/Chart.yaml` | Значения чарта-зависимости `universal-chart` (deployment, image, service, imagePullSecrets) |
| CI/CD | `.gitlab-ci.yml` | Выбор окружения по ветке/тегу, имя релиза/чарта, `HELM_SET_ARGS` |
| IaC (kustomize) | `iac/apps/stamp-verification/**` | Namespace, Deployment, Service и оверлеи кластеров (этот репозиторий) |
Отдельного конфиг-файла (yaml/toml/env) приложение не читает.
## Runtime-поведение (`static/js/index.js`)
Скрипт выполняется на `DOMContentLoaded`. Логика:
- читает query-параметры: `id` (идентификатор QR), `page_number`, `v` (пока не используется);
- запрос выполняется только если заданы одновременно `id` и `page_number`;
- выбирает базовый URL API:
| `window.location.host` | Базовый URL API | Окружение |
| --- | --- | --- |
| `stamp-verification.sarex.io` | `api.sarex.io` | prod |
| любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage |
- делает `fetch` `GET https://<apiBaseUrl>/documentations/api/v1/public/qr/{id}/document_info?number_page={page_number}` с `mode: cors`, `credentials: include`, `cache: no-cache`;
- по ответу заполняет поля страницы (название, версия, номер страницы, автор, дата загрузки), блок изменений (changelog), индикаторы актуальности/аннулирования версии, статус согласования и ссылку на документ.
Локаль дат — `ru` (`moment.locale("ru")`), формат отображения даты — `DD.MM.YYYY / HH:mm`.
Полный перечень внешних вызовов см. в `ENDPOINTS.md`, контракт эндпоинта — в `openapi.yaml`.
## nginx (`nginx.conf`)
| Параметр | Значение | Назначение |
| --- | --- | --- |
| `listen` | `8080` | Порт HTTP |
| `root` | `/dist` | Корень статики (туда Dockerfile кладёт `static/` и `index.html`) |
| `location = /ping` | `return 200 '{"result": "ok"}'` | Health-check для k8s-проб/балансировщика |
| `access_log` | `/dev/stdout` | Логи доступа в stdout |
| `error_log` | `stderr warn` | Логи ошибок в stderr |
| `gzip` | `on` | Сжатие ответов |
| `expires` | `off` | Без заголовков кеширования |
## Docker (`Dockerfile`, `docker-compose.yml`)
Образ на базе `nginx:mainline-alpine-otel`. Сборка копирует `nginx.conf` в `/etc/nginx/nginx.conf`, каталог `static` и `index.html` в `/dist`, выставляет права на `/var` и `/run`, объявляет `EXPOSE 8080` и запускает `nginx -g "daemon off;"`. Build-time аргументов нет (`BUILD_ARGS=""`).
`docker-compose.yml` — только для локального прогона (`image: sarex/landing:latest`, публикует порт `8000:8000`; обратите внимание, что nginx внутри слушает `8080` — маппинг в compose оставлен историческим).
## Helm / universal-chart (`.helm/`)
`Chart.yaml` подключает зависимость `universal-chart` (`oci://cr.yandex/crp3ccidau046kdj8g9q/charts`, версия `0.1.7`). Значения задаются в `values.yaml` под ключом `universal-chart` и разбиты по окружениям через суффиксы (`_default`, `stage`, `preprod`, `production`).
| Ключ (`universal-chart.services.frontend.*`) | Значение (`_default`) | Назначение |
| --- | --- | --- |
| `deployment.enabled` | `true` | Создавать Deployment |
| `deployment.name` | `stamp-verification-frontend` | Имя Deployment |
| `deployment.port` | `8080` | Порт контейнера |
| `deployment.replicaCount` | `1` | Число реплик |
| `image.name` | `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:latest` | Образ |
| `image.pullPolicy` | `IfNotPresent` | Политика загрузки образа |
| `service.enabled` | `true` | Создавать Service |
| `service.name` | `frontend-service` | Имя Service |
| `service.port` | `8080``production``80`) | Порт сервиса |
| `service.targetPort` | `8080` | Целевой порт |
| `service.type` | `ClusterIP` | Тип сервиса |
| `imagePullSecrets.name` | `dockerhub` | Секрет для доступа к реестру |
`global.env` (`_default`) выбирает активный набор значений по окружению.
## CI/CD (`.gitlab-ci.yml`)
Пайплайн подключает общие шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml`, ref `apps-business`). Общие переменные: `SERVICE_NAME=stamp-verification-frontend`, `DOCKERFILE_PATH=Dockerfile`, `BUILD_ARGS=""`, `CI_TRIGGER_SOURCE=app`.
Окружение переключается правилами `workflow.rules`:
| Условие | STAND | NAMESPACE | CHART_VERSION | K8S_HUSTLER_BRANCH |
| --- | --- | --- | --- | --- |
| `merge_request_event` | — | — | — | `ENABLE_BUILD_IMAGE="false"` (только проверки, без сборки образа) |
| ветка `stage` | `stage` | `documentations` | `0.0.1-stage` | `universal-chart-stage` |
| ветка `master` | `preprod` | `stamp-verification-preprod` | `0.0.1-preprod` | `universal-chart-preprod` |
| тег (`CI_COMMIT_TAG`) | `production` | `stamp-verification-prod` | `0.0.1-prod` | `universal-chart-production` |
| иначе | `when: never` | | | |
Для каждого деплой-окружения `HELM_SET_ARGS` передаёт: образ (`universal-chart.services.frontend.image.name.<env>=${IMAGE_NAME}`), `universal-chart.global.env=<env>` и метаданные коммита (`commitSha`, `gitlabUri`, `gitlabJobUrl`, `owner`).
## Инфраструктура (kustomize, этот репозиторий)
Каталог `iac/apps/stamp-verification/` содержит kustomize-манифесты (альтернатива/дополнение к Helm-деплою из CI):
- `base/namespace.yaml` — namespace `stamp-verification` с `istio-injection: enabled`;
- `base/deployment.yaml` — Deployment `frontend` (образ `cr.yandex/crp3ccidau046kdj8g9q/stamp-verification-frontend:<sha>`, `imagePullPolicy: IfNotPresent`, `containerPort: 80`, requests `cpu: 25m`/`memory: 100Mi`, `imagePullSecrets: regcred`);
- `base/service.yaml` — Service `frontend-service` (`ClusterIP`, `port: 80``targetPort: 80`);
- `base/kustomization.yaml` — сборка base (namespace + deployment + service);
- `yc-k8s-test/kustomization.yaml` — оверлей кластера (`resources: ../base`, патчи закомментированы);
- `yc-k8s-test/replicas.yaml` — заготовка патча реплик (`replicas: 1`).
## Замечания и потенциальные проблемы
- **Захардкоженный JWT.** В `static/js/index.js` в заголовок `Authorization: Bearer …` вшит истёкший (2023 г.) токен. Публичный эндпоинт `/public/qr/...` не должен требовать пользовательский токен — строку следует удалить. Держать секреты в статике недопустимо.
- **Рассогласование портов.** nginx и Helm/`universal-chart` используют порт **8080**, а kustomize-манифесты в этом репозитории (`base/deployment.yaml`, `base/service.yaml`) — порт **80**. Нужно привести к одному значению (образ слушает 8080), иначе проброс/пробы могут не сходиться.
- **Дублирование деплоя.** Приложение может разворачиваться и через Helm (CI, чарт `universal-chart`), и через kustomize (этот репозиторий). Следует зафиксировать единый источник истины, чтобы образ/порт/namespace не расходились.
- **NAMESPACE для stage.** На ветке `stage` деплой идёт в namespace `documentations` (общий), тогда как preprod/prod — в выделенные `stamp-verification-*`. Это осознанное решение или наследие — стоит проверить.
- **Конфиг только по хосту.** Выбор prod/stage жёстко завязан на строку `stamp-verification.sarex.io`. Любой новый прод-домен потребует правки кода, а не конфигурации.
- **Mock-данные в бандле.** В `index.js` присутствуют объекты `mockData`/`mockChangeLog` — тестовые данные, оставшиеся в продовом бандле. На поведение не влияют (не используются), но их стоит убрать.
## Минимальный набор для локального запуска
Отдельная конфигурация не требуется — приложение статично:
```
# сборка и запуск образа
docker build -t stamp-verification-frontend .
docker run --rm -p 8080:8080 stamp-verification-frontend
# проверка
curl http://localhost:8080/ping # {"result": "ok"}
# страница: http://localhost:8080/?id=<qrId>&page_number=1
```
Для реальных данных нужен доступный backend `documentations` (по умолчанию для не-prod хоста используется `https://stage-api.sarex.io`).

View File

@ -0,0 +1,69 @@
# Эндпоинты, с которыми взаимодействует stamp-verification-frontend
Документ описывает все внешние HTTP-вызовы, которые выполняет статическая страница проверки штампа (`stamp-verification-frontend`).
## Как устроено взаимодействие
В отличие от микрофронтендов с декларативным реестром эндпоинтов, здесь весь сетевой код сосредоточен в одном файле — `static/js/index.js`. Страница обычным `fetch` обращается к **одному** публичному эндпоинту backend-сервиса `documentations`, а также подгружает сторонние ресурсы аналитики и статику.
Логика вызова:
1. из query-строки берутся параметры `id` (идентификатор QR) и `page_number`; запрос выполняется только если оба заданы;
2. базовый хост API выбирается по `window.location.host`;
3. выполняется `GET`-запрос с `mode: "cors"`, `credentials: "include"`, `cache: "no-cache"`;
4. ответ (JSON) маппится на поля страницы.
## Базовые хосты по окружениям
Значение определяется в `index.js` по хосту страницы. Итоговый URL = `https://<базовый хост>` + путь эндпоинта.
| Хост страницы (`window.location.host`) | Базовый хост API | Окружение |
| --- | --- | --- |
| `stamp-verification.sarex.io` | `api.sarex.io` | prod |
| любой другой (напр. `stamp-verification.stage.sarex.io`) | `stage-api.sarex.io` | stage |
> Промежуточных окружений (preprod/local) в коде не предусмотрено: всё, что не prod-домен, трактуется как stage.
## Эндпоинты по сервисам
### `documentations` — Сервис документации
| Ключ | Метод | Путь | Назначение |
| --- | --- | --- | --- |
| `getQrDocumentInfo` | GET | `/documentations/api/v1/public/qr/{qrId}/document_info?number_page={pageNumber}` | Публичная информация о документе по QR-коду штампа: название, версия, автор, дата загрузки, актуальность/аннулирование версии, статус согласования, changelog и ссылка на документ в Sarex |
Параметры запроса:
| Параметр | Расположение | Обязателен | Назначение |
| --- | --- | --- | --- |
| `qrId` | path (`{qrId}`) | да | Идентификатор QR-кода (из query `?id=`) |
| `number_page` | query | да | Номер страницы документа (из query `?page_number=`) |
Заголовки запроса (как в текущем коде): `Content-Type: application/json` и `Authorization: Bearer <jwt>`. **Токен захардкожен и истёк** — см. раздел «Замечания». Запрос идёт с `credentials: "include"` (куки/учётные данные), `redirect: "follow"`, `referrerPolicy: "no-referrer"`.
Используемые поля ответа (по `index.js`): `document_name`, `version_number`, `page_number`, `author`, `upload_date`, `is_actual_version`, `is_canceled`, `does_bundle_have_flows`, `is_actual_version_approved`, `document_review_status`, `document_url`, `is_latest_changelog`, `changelog` (`no`, `description`, `author` и др.). Полная схема — в `openapi.yaml`.
Формирование ссылки на документ: при наличии `document_url` ссылка на странице собирается как `${data.document_url}&page_number=${data.page_number}` и открывается в Sarex.
## Внешние (сторонние) ресурсы
Не относятся к API Sarex, но выполняются страницей:
| Ресурс | Метод | URL | Назначение |
| --- | --- | --- | --- |
| Яндекс.Метрика (tag.js) | GET | `https://mc.yandex.ru/metrika/tag.js` | Загрузка счётчика аналитики (id `93441026`) |
| Яндекс.Метрика (noscript) | GET | `https://mc.yandex.ru/watch/93441026` | Пиксель для окружения без JS |
| Статика приложения | GET | `static/css/*`, `static/js/*`, `static/icons/*`, `static/fonts/*`, `static/img/*` | CSS, `moment-with-locales.min.js`, иконки, шрифты, фон — раздаются самим nginx |
Health-check (со стороны инфраструктуры, не самой страницы): `GET /ping``200 {"result": "ok"}` (отдаётся nginx).
## Обработка ошибок
Специализированного маппинга кодов ошибок в человекочитаемые сообщения (как в backend-сервисах) в коде нет. Ошибки обрабатываются минимально: `fetch` обёрнут в `.catch(...)`, ошибки логируются в консоль (`console.error`). При пустом/отсутствующем ответе поля страницы заполняются плейсхолдером `` (`PLACEHOLDER_STR`). Пользовательских уведомлений об ошибке нет.
## Замечания
- **Захардкоженный JWT.** В заголовке `Authorization` в `index.js` вшит истёкший (2023 г.) Bearer-токен. Эндпоинт публичный (`/public/qr/...`) — заголовок с токеном следует удалить; хранить токены в статике недопустимо.
- **`credentials: "include"`** при кросс-доменном запросе к `*-api.sarex.io` требует корректной CORS-конфигурации на стороне `documentations` (`Access-Control-Allow-Credentials` + конкретный `Access-Control-Allow-Origin`).
- **Параметр `v`** (версия) читается из query, но пока не используется.
- **Выбор окружения только по домену** — новый prod-домен потребует правки кода.

View File

@ -0,0 +1,216 @@
openapi: 3.0.3
info:
title: Stamp Verification — Public QR API (consumed)
version: "1.0.0"
description: |
Контракт публичного эндпоинта, который потребляет фронтенд
**stamp-verification-frontend** (`pdm/stamp-verification-frontend`).
Сам фронтенд — статическая страница на nginx (ванильный JS, `static/js/index.js`)
и **не предоставляет** собственного HTTP API. Данная спецификация описывает
единственный внешний эндпоинт backend-сервиса **documentations**, к которому
страница обращается для получения информации о документе по QR-коду штампа.
Спецификация восстановлена по клиентскому коду (`index.js`) и служит
справочным контрактом (потребитель, а не поставщик).
### Базовый хост
Выбирается на клиенте по `window.location.host`:
- `stamp-verification.sarex.io` → `https://api.sarex.io` (prod);
- любой другой хост → `https://stage-api.sarex.io` (stage).
### Аутентификация
Эндпоинт публичный (сегмент пути `/public/`). Текущий клиент тем не менее
отправляет заголовок `Authorization: Bearer <jwt>` с **захардкоженным и
истёкшим** токеном — это дефект клиента, а не требование API (см. замечания
в `ENDPOINTS.md`). Запрос выполняется с `credentials: include` (CORS с
учётными данными).
### Формат
Ответ — `application/json`. Все поля опциональны/nullable: клиент подставляет
плейсхолдер `` при отсутствии значения.
servers:
- url: https://api.sarex.io
description: prod
- url: https://stage-api.sarex.io
description: stage
paths:
/documentations/api/v1/public/qr/{qr_id}/document_info:
get:
operationId: getQrDocumentInfo
summary: Информация о документе по QR-коду штампа
description: |
Возвращает публичную информацию о документе, на который указывает QR-код
штампа: название, версию, автора, дату загрузки, актуальность и статус
согласования версии, сведения об изменениях (changelog) и ссылку на
документ в Sarex.
tags:
- public-qr
parameters:
- name: qr_id
in: path
required: true
description: Идентификатор QR-кода (на клиенте — query-параметр `id`).
schema:
type: string
example: "b46d6b82-9e51-48e3-8dce-a38dfc6b2a26"
- name: number_page
in: query
required: true
description: Номер страницы документа (на клиенте — query-параметр `page_number`).
schema:
type: integer
minimum: 1
example: 1
responses:
"200":
description: Информация о документе
content:
application/json:
schema:
$ref: "#/components/schemas/QrDocumentInfo"
"404":
description: Документ/QR не найден
"422":
description: Некорректные параметры запроса
"500":
description: Внутренняя ошибка сервиса
components:
schemas:
QrDocumentInfo:
type: object
description: |
Набор полей, используемых клиентом (`static/js/index.js`). Поля
опциональны — при отсутствии клиент отображает плейсхолдер ``.
properties:
document_name:
type: string
nullable: true
description: Имя файла документа
example: some-file-01.pdf
version_number:
type: string
nullable: true
description: Номер версии документа
example: v2
page_number:
type: integer
nullable: true
description: Номер страницы (эхо параметра запроса); участвует в сборке ссылки на документ
example: 1
author:
type: string
nullable: true
description: Автор версии (отображаемое имя)
example: Андрей Алешков
upload_date:
type: string
format: date-time
nullable: true
description: Дата и время загрузки версии (ISO 8601); на клиенте форматируется как `DD.MM.YYYY / HH:mm`
example: "2023-05-23T20:01:55.496768Z"
is_actual_version:
type: boolean
nullable: true
description: >-
`true` — последняя версия; `false` — не последняя; отсутствие/`null` —
актуальность неизвестна
example: true
is_canceled:
type: boolean
nullable: true
description: >-
`true` — QR-код/версия аннулированы (клиент показывает предупреждение
«QR-код аннулирован»)
example: false
does_bundle_have_flows:
type: boolean
nullable: true
description: Есть ли у бандла процессы согласования (управляет показом блока статуса согласования)
example: false
is_actual_version_approved:
type: boolean
nullable: true
description: Согласована ли текущая версия (при `does_bundle_have_flows = true`)
example: true
document_review_status:
type: string
nullable: true
description: Человекочитаемый статус документа (показывается при согласованной актуальной версии)
example: Согласован
document_url:
type: string
format: uri
nullable: true
description: >-
Ссылка на документ в Sarex. Клиент открывает её как
`${document_url}&page_number=${page_number}`
example: https://stage.sarex.io/workspaces-v2/0244d9f9-8f32-4b65-80c7-7c403b85c81e?type=pdf
is_latest_changelog:
type: boolean
nullable: true
description: >-
Является ли `changelog` последним. Если `false` при наличии
`changelog` — клиент показывает предупреждение «Была выпущена новая
версия изменения»
example: true
changelog:
$ref: "#/components/schemas/Changelog"
Changelog:
type: object
nullable: true
description: Сведения об изменении версии документа
properties:
bundle_id:
type: string
format: uuid
example: b46d6b82-9e51-48e3-8dce-a38dfc6b2a26
"no":
type: string
description: Порядковый номер изменения (на клиенте выводится как `№{no}`)
example: "2"
description:
type: string
nullable: true
description: >-
Описание изменения. Если длиннее 20 символов — клиент включает
сворачивание текста; при отсутствии показывает «Нет описания изменений»
example: Внесены изменения в структуру документа. Добавлена подпись руководителя проекта.
created_at:
type: string
format: date-time
example: "2023-05-23T20:01:55.496768Z"
updated_at:
type: string
format: date-time
example: "2023-05-23T20:01:55.496768Z"
created_by:
type: integer
example: 2203
author:
$ref: "#/components/schemas/ChangelogAuthor"
ChangelogAuthor:
type: object
description: Автор изменения
properties:
id:
type: integer
example: 2203
username:
type: string
example: a.aleshkov
email:
type: string
format: email
example: a.aleshkov@sarex.io
first_name:
type: string
example: Андрей
last_name:
type: string
example: Алешков

View File

@ -0,0 +1,906 @@
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