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 `. Токен также может передаваться 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