---
layout: 'page'
uri: '/skills/gk-queries'
position: 30
slug: 'skills-gk-queries'
parent: 'skills-cqrs'
navTitle: 'gk-queries'
title: 'GK — Query handlers (read operace)'
description: 'Read operace (čtení dat bez změny stavu) — struktura query handleru, deklarace permission a typovaný návrat přes bus.Query. Use when přidáváš endpoint, který něco čte/vypisuje (list, detail, dashboard), a řešíš strukturu handleru, permission nebo jak ho poslat přes bus.'
name: 'gk-queries'
---
# GK — Query handlers (read operace)
Query čte stav systému a **nic nemění**. V gokicku má pevnou strukturu, deklaruje
permission a posílá se přes `QueryBus` typovaným `bus.Query`.
## What & when
- Sáhni sem, když přidáváš čtecí endpoint: výpis (`ListUsers`), detail, dashboard —
cokoli, co jen vrací data a nezapisuje do DB.
- **Netýká se zápisu.** Cokoli, co mění stav (create/update/delete), je *command* →
`/gk-bus`. Rozdíl: command jde přes transakci + audit + eventy, query ne.
- Pokud řešíš celý průchod busem a middleware → `/gk-bus`. Pokud přidáváš featuru
napříč vrstvami → `/gk-feature`.
## For non-tech / juniors
Představ si knihovnu. **Query** je „přijdu k pultu a zeptám se: ukaž mi seznam knih"
— jen čtu, nic nepřesouvám. **Command** je „chci si knihu půjčit" — mění se stav
(kniha je teď u mě). Gokick to dělí schválně (vzor zvaný **CQRS** — Command Query
Responsibility Segregation): čtení a zápis mají jiná pravidla, takže je drží
oddělené. Čtení nepotřebuje transakci ani záznam do audit logu — jen ověří, že na to
máš oprávnění (permission), a vrátí data.
## How it works
Query žije v `app/application/<context>/query/` — např.
`app/application/user/query/list_users.go`,
`app/application/dashboard/query/get_admin_dashboard.go`.
**Tři části** (viz `list_users.go`):
```go
// 1) Query struct — drží parametry tak, jak přišly po drátě (syrové stringy).
type ListUsersQuery struct {
Page int
PerPage int
SortBy string
SortDir string
Nickname string
Email string
Role string
Active string
}
// 2) Permission — POVINNÉ (viz Invariants).
func (ListUsersQuery) RequiredPermission() string { return "admin:users:read" }
// 3) Handler — konstruktor bere DOMÉNOVÉ rozhraní, ne konkrétní repo.
type ListUsersHandler struct{ users user.Repository }
func NewListUsersHandler(users user.Repository) *ListUsersHandler {
return &ListUsersHandler{users: users}
}
func (h *ListUsersHandler) Handle(ctx context.Context, q ListUsersQuery) (user.ListPage, error) {
// Handle syrový vstup normalizuje do whitelistovaných doménových kritérií:
// neznámý sloupec/směr a stránka mimo rozsah spadnou na default, ne na 400
// — řazení a stránkování jsou UX preference, ne kontrakt.
criteria := user.ListCriteria{
Page: q.Page,
PerPage: q.PerPage,
Sort: user.SortColumnFrom(q.SortBy),
SortDir: shared.SortDirectionFrom(q.SortDir),
Filters: user.ListFilters{
Nickname: q.Nickname,
Email: q.Email,
Role: q.Role,
Active: q.Active,
},
}.Normalize()
return h.users.FindPage(ctx, criteria)
}
```
**Návratový typ je libovolný** — stránka dat (`user.ListPage`, tj. `Items` + `Total`)
i vlastní DTO struct (`AdminDashboard{ UsersActive, UsersTotal int }`
v `get_admin_dashboard.go`).
**Dispatch z HTTP handleru** přes generický `bus.Query[R]`
(`app/application/bus/dispatch.go`) — `R` je typ výsledku, takže návrat je typovaný.
`bus.Query` bere přímo `*bus.QueryBus` (vnitřní `*Bus` je neexportovaný), takže párování
bus↔operace hlídá kompilátor — query nejde omylem poslat na command bus:
```go
// app/presentation/http/handler/dashboard.go
result, err := bus.Query(
r.Context(),
h.queryBus, // *bus.QueryBus
"GetAdminDashboard", // jméno do logů
q, // samotná query (nese permission)
func(ctx context.Context) (dashboardqry.AdminDashboard, error) {
return h.adminDash.Handle(ctx, q)
},
)
```
**QueryBus middleware chain** je krátký a jen čtecí — `BaseChain` v
`app/application/bus/middleware/base.go`:
```
Recovery → Logging → Authorize → Tenant
```
`Tenant` (`middleware/tenant.go`) hned po autorizaci resolvuje aktivní tenant do ctx — čtení potřebuje tenant scoping stejně jako zápis (viz `/gk-multitenancy`).
Žádná transakce, žádný audit, žádné eventy (to mají jen command busy — `CommandBus` a `SystemCommandBus`). `Authorize`
(`middleware/authorize.go`) zavolá `RequiredPermission()` a ověří ho proti rolím
volajícího.
## Recipe
Přidání nové query (čtecí endpoint):
1. **Soubor** `app/application/<context>/query/<verb>_<noun>.go` (např.
`user/query/list_users.go`).
2. **Query struct** s filtry (klidně prázdný `struct{}`).
3. **Permission**: přidej `RequiredPermission() string` (vrať existující permission
string), nebo u veřejné query `func (Q) SkipPermissionCheck() {}`.
4. **Handler** + `NewXxxHandler(...)` konstruktor; závislosti ber jako **doménová
rozhraní** (`user.Repository`), ne konkrétní `*sqliteuser.Repository`.
5. **`Handle(ctx, q) (R, error)`** — jen čte, žádné zápisy.
6. **HTTP handler** v `presentation/http/handler/` dispatchni přes
`bus.Query[R](ctx, h.queryBus, "Name", q, fn)`; výsledek namapuj na DTO a vrať
`h.resp.JSON(r.Context(), w, http.StatusOK, dto)`, chybu přes `h.resp.HandleError(r.Context(), w, err)` (`resp *response.Responder` si handler nechá injektovat v konstruktoru).
7. **Route** zaregistruj v `presentation/http/server/server.go`, **DI** dráty
(provider handleru + query handleru) v `infrastructure/di/container_provider.go`,
pak `make di`. Celý průchod vrstvami → `/gk-feature`.
## Invariants & pitfalls
- **Permission je povinná.** Každá query MUSÍ implementovat buď `Permissioned`
(`RequiredPermission`), nebo `SkipPermission` (`SkipPermissionCheck`). Když chybí
obojí, `AuthorizeMiddleware` vrátí error (`bus: command %q must implement …`) —
chrání před zapomenutou deklarací.
- **Query nemá side-effects.** Jen čte. Žádné `Save`/zápis do DB, žádné sbírání
doménových eventů (`EventCollector` patří commandům). Pokud potřebuješ zápis, je to
command, ne query.
- **Závislost na doménovém rozhraní, ne na konkrétním repu.** Handler drží
`user.Repository`, ne `*sqliteuser.Repository` — jinak porušíš pravidlo vrstev
(`make arch-check` to chytne).
- **Nikdy nevolej handler napřímo z HTTP** — vždy přes `bus.Query`. Mimo bus by
permission check ani logging neproběhly.
- **Žádné raw permission stringy na frontendu.** Backendová query deklaruje string
(`admin:users:read`), FE musí stejnou permission brát z `Permission` enumu
(`assets/app/Auth/enums/resources.ts`) — viz `CLAUDE.md`.
## Related
- Skills: `/gk-bus` (busy + middleware chain + dispatch), `/gk-feature` (query
end-to-end přes vrstvy), `/gk-entities` (entity a value objects, které query vrací)
- Kód: `app/application/user/query/list_users.go`,
`app/application/dashboard/query/get_admin_dashboard.go`,
`app/application/bus/dispatch.go`, `app/application/bus/middleware/base.go`,
`app/presentation/http/handler/dashboard.go`
---
[← gk-commands](/skills/gk-commands.md) | [Data & infrastruktura →](/skills/data.md)