GraphQL-сервис на Go для постов и иерархических комментариев.
- создание и чтение постов;
- включение и выключение комментариев только автором поста;
- создание комментариев первого уровня и ответов на комментарии;
- неограниченная вложенность комментариев на уровне модели данных;
- постраничное чтение комментариев по одному уровню дерева;
- GraphQL subscriptions для новых комментариев под конкретным постом;
- два режима хранения:
memoryиpostgres; - Dockerfile и docker-compose для запуска сервиса;
Локально с in-memory storage:
STORAGE_TYPE=memory go run ./cmd/serverЧерез Docker Compose с PostgreSQL:
docker compose up --buildGraphQL playground будет доступен на:
http://localhost:8080/GraphQL endpoint:
http://localhost:8080/queryHealthcheck:
http://localhost:8080/healthВсе мутации требуют HTTP-заголовок X-User-ID.
В GraphQL Playground заголовок задается в панели Headers:
{
"X-User-ID": "user-1"
}При создании поста X-User-ID сохраняется как authorID. Изменить commentsEnabled может только пользователь с тем же ID. Имя автора остается отображаемым полем и не используется для проверки прав.
Переменные окружения:
HTTP_ADDR=:8080
STORAGE_TYPE=memory
POSTGRES_DSN=postgres://postservice:postservice@localhost:5432/postservice?sslmode=disableSTORAGE_TYPE может быть:
memory;postgres.
Если выбран postgres, переменная POSTGRES_DSN обязательна.
Комментарии хранятся через модель parent_id.
Комментарий первого уровня имеет parent_id = NULL. Ответ на комментарий содержит parent_id родительского комментария.
В PostgreSQL основная таблица комментариев содержит:
id;post_id;parent_id;author_id;author_name;text;created_at.
Для быстрого чтения веток создан индекс:
(post_id, parent_id, created_at, id)In-memory storage устроен похожим образом: комментарии лежат в map по id, а отдельный индекс группирует id комментариев по паре postID + parentID. Поэтому при чтении ответов не нужно сканировать все комментарии.
Создание комментария атомарно проверяет существование поста, флаг comments_enabled, существование родителя и принадлежность родителя тому же посту. В PostgreSQL проверка и вставка выполняются в одной транзакции с блокировкой строки поста. В memory-режиме используется общий RWMutex постов и блокировка индекса комментариев.
На уровне PostgreSQL принадлежность родителя тому же посту дополнительно защищена составным внешним ключом (parent_id, post_id).
Комментарии не возвращаются одним бесконечным деревом. Клиент запрашивает конкретный уровень:
- комментарии первого уровня под постом;
- ответы на конкретный комментарий;
- следующую страницу той же ветки.
Это защищает сервис от тяжелых запросов при большой вложенности и большом количестве комментариев.
Текущая пагинация использует limit и offset.
Ограничения:
- значение по умолчанию:
20; - максимум:
100; - отрицательные значения запрещены.
Основные queries:
query {
posts(limit: 20, offset: 0) {
id
authorID
title
authorName
commentsEnabled
createdAt
}
}query {
comments(postID: "post-id", parentID: null, limit: 20, offset: 0) {
id
parentID
authorName
text
createdAt
}
}Основные mutations:
mutation {
createPost(input: {authorName: "Alice", title: "Hello", text: "First post"}) {
id
title
}
}mutation {
createComment(input: {postID: "post-id", authorName: "Bob", text: "Nice post"}) {
id
text
}
}Subscription:
subscription {
commentAdded(postID: "post-id") {
id
text
authorName
}
}Основные пакеты:
cmd/server- точка входа;internal/config- конфигурация через env;internal/auth- идентичность пользователя вcontext;internal/domain- сущности и доменные ошибки;internal/service- бизнес-логика;internal/storage- интерфейсы хранилища;internal/storage/memory- in-memory реализация;internal/storage/postgres- PostgreSQL реализация и миграции;internal/graph- GraphQL-схема и резолверы;internal/pubsub- subscriptions для новых комментариев.
WebSocket subscription commentAdded(postID) получает новые комментарии только выбранного поста. У каждого клиента есть ограниченный буфер. Если клиент перестает читать и переполняет его, subscription закрывается явно: клиент должен переподключиться и перечитать комментарии через query. Благодаря этому медленный клиент не блокирует создание комментариев и не вызывает неограниченный рост памяти.
Unit-тесты, race detector и статический анализ:
go test ./...
go test -race ./...
go vet ./...PostgreSQL integration-тест запускается при наличии отдельной тестовой базы:
TEST_POSTGRES_DSN='postgres://postservice:postservice@localhost:5432/postservice?sslmode=disable' \
go test ./internal/storage/postgresТесты покрывают бизнес-правила, Unicode-ограничение комментариев, владельца поста, пагинацию, конкурентный memory storage, GraphQL-контракт, коды ошибок, конфигурацию и subscriptions.