Skip to content

bukleeff-ux/orgapi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Organizational Structure API

Тестовое задание: REST API организационной структуры компании (подразделения с самовложенным деревом + сотрудники).

Стек

Слой Технология
Язык Go 1.22
HTTP net/http (стандартный ServeMux с path-параметрами)
ORM GORM
БД PostgreSQL 16
Миграции goose (SQL-миграции)
Логирование log/slog (структурированный JSON)
Тесты net/http/httptest + testify
Деплой Docker + docker-compose

Быстрый старт

git clone https://github.com/bukleeff-ux/orgapi
cd orgapi
docker-compose up --build

Через 5–10 секунд API будет доступен по адресу http://localhost:8080. Postgres поднимется в том же compose; миграции накатываются автоматически при старте контейнера API (см. scripts/entrypoint.sh).

Проверка живости:

curl http://localhost:8080/healthz
# → ok

Тесты

Запуск без Docker, локально:

go test ./...

Тесты не требуют Postgres — HTTP-слой проверяется через httptest с мок-сервисами, бизнес-логика покрыта табличными тестами на уровне сервиса.

Структура проекта

.
├── cmd/api/                 точка входа: main.go
├── internal/
│   ├── config/              загрузка ENV-переменных
│   ├── apperror/            типизированные доменные ошибки
│   ├── domain/              модели Department, Employee
│   ├── repository/          GORM-обёртки, без бизнес-логики
│   ├── service/             бизнес-логика: валидация, циклы, транзакции
│   ├── middleware/          HTTP-middleware (логгер запросов)
│   └── handler/             HTTP-хендлеры + роутер на ServeMux
├── migrations/              goose SQL-миграции
├── scripts/entrypoint.sh    init контейнера: wait-for-pg → goose up → api
├── tests/                   httptest + testify
├── Dockerfile               multi-stage build
├── docker-compose.yml
└── README.md

Архитектура — классический трёхслойный backend: handler → service → repository. Каждый слой зависит только от слоя ниже, бизнес-логика изолирована от HTTP и от GORM. Это даёт лёгкое мокание (см. тесты) и независимость от выбора HTTP-фреймворка или ORM.

API

Все ответы — JSON. Ошибки приходят в формате {"error": "..."}.

1. Создать подразделение

POST /departments/
Content-Type: application/json

{
  "name": "Backend",
  "parent_id": 1
}

Тело:

  • name — строка 1..200, обязательна.
  • parent_id — uint или null. Если задан — депортамент должен существовать.

Ответ 201 Created:

{ "id": 5, "name": "Backend", "parent_id": 1, "created_at": "..." }

2. Получить подразделение с поддеревом

GET /departments/{id}?depth=2&include_employees=true

Query:

  • depth — 0..5 (по умолчанию 1). Глубина поддерева в ответе.
  • include_employeestrue|false|1|0 (по умолчанию true).

Ответ 200 OK:

{
  "id": 1,
  "name": "Company",
  "parent_id": null,
  "created_at": "...",
  "employees": [ ... ],
  "children": [
    {
      "id": 2,
      "name": "R&D",
      "employees": [ ... ],
      "children": [ ... ]
    }
  ]
}

3. Создать сотрудника в подразделении

POST /departments/{id}/employees/
Content-Type: application/json

{
  "full_name": "Иван Иванов",
  "position": "Backend Engineer",
  "hired_at": "2024-09-01"
}

hired_at — формат YYYY-MM-DD, опционален.

Ответ 201 Created с созданным сотрудником.

4. Переименовать / переместить подразделение

PATCH /departments/{id}
Content-Type: application/json

{
  "name": "Backend Platform",
  "parent_id": 7
}

Любое из полей опционально. Чтобы перенести в корень — "parent_id": null.

5. Удалить подразделение

DELETE /departments/{id}?mode=cascade
DELETE /departments/{id}?mode=reassign&reassign_to_department_id=10
  • mode=cascade (по умолчанию) — каскадно удаляет депортамент, все его подразделения и всех сотрудников. Каскад реализован на уровне БД через ON DELETE CASCADE в foreign keys.
  • mode=reassign — переводит сотрудников в reassign_to_department_id, затем удаляет депортамент. Допустимо только если у депортамента нет дочерних подразделений (иначе вернётся 409 Conflict).

Ответ 204 No Content.

Бизнес-правила и коды ошибок

Ситуация Код
Депортамент / сотрудник / родитель не найден 404
name пустое или длиннее 200 символов 400
Невалидный JSON, неверный формат даты 400
mode=reassign без reassign_to_department_id 400
Имя депортамента не уникально в пределах parent_id 409
Депортамент сам себе родитель 409
Перемещение создаёт цикл в дереве 409
mode=reassign для депортамента с дочерними 409
Внутренняя ошибка 500

Решения по реализации

  • Уникальность имени в пределах parent. Реализована через два частичных UNIQUE INDEX в PostgreSQL — для корневых (parent_id IS NULL) и для потомков (parent_id IS NOT NULL). Это надёжнее, чем проверка только на уровне сервиса (защищает от race condition).
  • Каскадное удаление. На уровне FOREIGN KEY ... ON DELETE CASCADE. Удаление родителя автоматически уничтожает поддерево и всех сотрудников одним SQL-запросом, без рекурсии в Go.
  • Детектирование цикла. Перед PATCH-перемещением выполняется PostgreSQL WITH RECURSIVE — собирается поддерево перемещаемого узла, и проверяется, не входит ли новый родитель в это поддерево.
  • PATCH с nullable полем. Для parent_id в PATCH важно различать «поле не передали» и «передали null». Тело декодируется в map[string]json.RawMessage, и наличие ключа определяется через проверку ok в map.
  • Graceful shutdown. На SIGTERM/SIGINT сервер завершает активные запросы в течение 10 секунд.
  • Retry подключения к Postgres. В compose-окружении API может стартануть до того, как Postgres примет подключения; есть 30 попыток по 1 секунде.

Что можно улучшить дальше

  • Pagination на сотрудников при include_employees=true.
  • OpenAPI / Swagger спецификация.
  • Заменить рекурсивный обход поддерева в Go на одну CTE-выгрузку.
  • Дополнительные тесты на уровне сервиса с реальным Postgres через testcontainers-go.
  • Метрики Prometheus, structured tracing.
  • Rate limiting на write-эндпоинтах.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages