一键导入
grain-query-handler
Implement query handlers that read from event-sourced read models and render hiccup
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implement query handlers that read from event-sourced read models and render hiccup
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Give your AI agents something more useful than a prompt. Velocity through clarity.
Visual quality gate — screenshot each primary screen, critique it against the baseline rubric, and refine the components until it passes. Use before declaring a build done.
Implement command handlers that validate state, emit events, and return signals using the Grain framework
Create scheduled background jobs that run on cron schedules
Build read models by reducing events into queryable state with defreadmodel
Define Malli schemas for commands, events, and queries using defschemas
| name | grain-query-handler |
| description | Implement query handlers that read from event-sourced read models and render hiccup |
(ns cjbarre.grain-todo-list.service.todo-list-service.queries
(:require [ai.obney.grain.datastar.ui :as ds-ui]
[ai.obney.grain.query-processor.interface :refer [defquery]]
[cjbarre.grain-todo-list.service.todo-list-service.read-models :as rm]
[cjbarre.grain-todo-list.service.todo-list-service.ui :as ui]))
Every page query lowers its view to HTML through a small render helper backed
by ds-ui/hiccup:
(defn render [page]
(ds-ui/hiccup page))
A page query declares its route, title, and the read models it depends on, then
returns both a :query/result and rendered :datastar/hiccup:
(defquery :todo tasks-page
{:authorized? authenticated?
:datastar/path "/tasks"
:datastar/title "Tasks"
:grain/read-models {:todo/tasks 1 :todo/projects 1}}
[ctx]
(let [tasks (vec (rm/active-tasks ctx))]
{:query/result {:tasks tasks}
:datastar/hiccup (render (ui/tasks-page {:tasks tasks}))}))
The home page is just the query mounted at /:
(defquery :todo home-page
{:authorized? authenticated?
:datastar/path "/"
:datastar/title "Grain Todo"
:grain/read-models {:todo/tasks 1 :todo/projects 1 :todo/weekly-review 1}}
"Main todo workspace."
[ctx]
(let [data (workspace-data ctx)]
{:query/result data
:datastar/hiccup (render (ui/home-page data))}))
{:query/result {:tasks tasks :projects projects}
:datastar/hiccup (render (ui/tasks-page {:tasks tasks :projects projects}))}
{::anom/category ::anom/not-found
::anom/message "Human readable message"}
Routes use query params, NEVER path params. Declare a static
:datastar/path and read params from (:query ctx):
(defquery :todo task-page
{:authorized? owns-task-query?
:datastar/path "/task" ;; NOT "/task/:task-id"
:datastar/title "Task"
:grain/read-models {:todo/tasks 1 :todo/projects 1}}
[{{:keys [task-id]} :query :as ctx}] ;; from /task?task-id=<uuid>
(let [task (get (rm/all-tasks ctx) task-id)]
{:query/result {:task task}
:datastar/hiccup (render (ui/task-page {:task task}))}))
Load all data a screen needs in one query, usually via a shared data-builder fn:
(defn workspace-data [ctx]
{:tasks (vec (rm/active-tasks ctx))
:due-soon (vec (rm/due-soon-tasks ctx))
:projects (vec (rm/active-project-summaries ctx))
:review (rm/current-weekly-review ctx)})
(defquery :todo review-page
{:authorized? authenticated?
:datastar/path "/review"
:datastar/title "Weekly Review"
:grain/read-models {:todo/tasks 1 :todo/projects 1 :todo/weekly-review 1}}
[ctx]
(let [data (workspace-data ctx)]
{:query/result data
:datastar/hiccup (render (ui/review-page data))}))
Enrich one read model with data from another inside the query:
(defquery :todo project-page
{:authorized? owns-project-query?
:datastar/path "/project"
:datastar/title "Project"
:grain/read-models {:todo/tasks 1 :todo/projects 1}}
[{{:keys [project-id]} :query :as ctx}]
(let [project (get (rm/all-projects ctx) project-id)
tasks (vec (rm/tasks-for-project ctx project-id))
project (when project
(assoc project :task-counts (rm/project-task-counts ctx project-id)))]
{:query/result {:project project :tasks tasks}
:datastar/hiccup (render (ui/project-page {:project project :tasks tasks}))}))
Page queries are event-driven. Declare :grain/read-models {<rm> <version>}
so the page re-renders only when those read models change — no polling.
:datastar/fps for normal state. Omitting both
:grain/read-models and :datastar/fps silently defaults to 30fps polling.:datastar/fps is :datastar/fps 0 for a
genuinely static page (e.g. the dev gallery), which disables re-rendering
entirely:(defquery :todo dev-gallery-page
{:authorized? (constantly true)
:datastar/path "/dev/gallery"
:datastar/title "Dev UI Gallery"
:datastar/fps 0} ;; static page — no re-render
[_ctx]
{:query/result {}
:datastar/hiccup (render (ui/dev-gallery-page))})
Public/landing pages may use :authorized? (constantly true). Any query reading
per-user / account-scoped data uses a real predicate. This repo uses:
authenticated? — requires a current userowns-task-query? / owns-project-query? — verify the queried entity belongs
to the caller (they read the param from (:query ctx) and check the
projection)(defn owns-task-query? [ctx]
(and (authenticated? ctx)
(if-let [task-id (get-in ctx [:query :task-id])]
(contains? (rm/all-tasks ctx) task-id)
true)))
:datastar/path "/task" + read (:task-id (:query ctx)) from /task?task-id=<uuid>:grain/read-models {<rm> <version>}; never :datastar/fps for normal state (only :datastar/fps 0 for static pages)(constantly true) only for public pages; per-user data uses authenticated? / ownership predicatesrender/ds-ui/hiccupctx) to get current state{:query/result {...} :datastar/hiccup (render ...)} for pagessrc/cjbarre/grain_todo_list/service/todo_list_service/queries.clj - Todo page queries (home, tasks, task, projects, project, review, dev gallery)src/cjbarre/grain_todo_list/service/user_service/queries.clj - Auth pages (good example of :authorized? predicates and :datastar/fps 0)