ワンクリックで
grain-read-model
Build read models by reducing events into queryable state with defreadmodel
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Build read models by reducing events into queryable state with defreadmodel
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
Implement query handlers that read from event-sourced read models and render hiccup
Define Malli schemas for commands, events, and queries using defschemas
| name | grain-read-model |
| description | Build read models by reducing events into queryable state with defreadmodel |
(ns cjbarre.grain-todo-list.service.todo-list-service.read-models
(:require [ai.obney.grain.read-model-processor-v2.interface :as rmp
:refer [defreadmodel]]))
This repo registers read models with defreadmodel (a cached, event-driven
projection) rather than reducing the event store by hand in each helper.
Define which events affect each read model:
(def task-event-types
#{:todo/task-captured
:todo/task-renamed
:todo/task-assigned-to-project
:todo/task-completed
:todo/task-archived
:todo/task-canceled
:todo/task-reactivated})
(def project-event-types
#{:todo/project-created
:todo/project-renamed
:todo/project-completed
:todo/project-canceled
:todo/project-reactivated})
;; Dispatch on event type
(defmulti tasks* (fn [_state event] (:event/type event)))
;; Handle creation
(defmethod tasks* :todo/task-captured
[state {:keys [user-id task-id title status order project-id]}]
(assoc state task-id
(cond-> {:user-id user-id
:task-id task-id
:title title
:status status
:order order}
project-id (assoc :project-id project-id))))
;; Handle rename
(defmethod tasks* :todo/task-renamed
[state {:keys [task-id title]}]
(assoc-in state [task-id :title] title))
;; Handle assignment
(defmethod tasks* :todo/task-assigned-to-project
[state {:keys [task-id project-id]}]
(assoc-in state [task-id :project-id] project-id))
;; Handle status transition
(defmethod tasks* :todo/task-completed
[state {:keys [task-id]}]
(assoc-in state [task-id :status] :completed))
;; Default: ignore unknown events
(defmethod tasks* :default [state _event] state)
defreadmodel ties the dispatch multimethod, the event-type set, and a version
together. The framework maintains the cached projection and rebuilds it from
events as needed:
(defreadmodel :todo tasks
{:events task-event-types :version 1}
[state event]
(tasks* state event))
{uuid1 {...} uuid2 {...}}.:default method so unrelated events are ignored.:version whenever the reducer's behavior changes in a way that
should rebuild the cached projection (new fields, changed shape, etc.).read-model-schemas group in schemas.clj (e.g. :todo/tasks [:map-of :uuid ::task]).Helpers take ctx (the full context) and read the cached projection via
rmp/project — never event-store/read directly:
;; Whole projection (a map keyed by entity id)
(defn all-tasks [ctx]
(rmp/project ctx :todo/tasks))
;; A single entity, using tag-scoped read options
(defn get-task [ctx task-id]
(get (rmp/project ctx :todo/tasks {:tags #{[:task task-id]}}) task-id))
Helpers derive views over the projection. They all take ctx:
(defn all-tasks [ctx]
(vals (rmp/project ctx :todo/tasks)))
(defn active-task? [task] (= :active (:status task)))
(defn active-tasks
"Active tasks, ordered."
[ctx]
(->> (vals (rmp/project ctx :todo/tasks))
(filter active-task?)
(sort-by (juxt :order :title :task-id))))
Use read options on rmp/project to scope by tag instead of scanning
everything:
(defn user-scope [ctx]
(when-let [user-id (:current-user/id ctx)]
{:tags #{[:user user-id]}}))
(defn tasks-for-user [ctx]
(rmp/project ctx :todo/tasks (user-scope ctx)))
Combine multiple read models in a single helper, each via rmp/project:
(defn project-task-counts
"Count tasks by status for one project."
[ctx project-id]
(->> (vals (rmp/project ctx :todo/tasks))
(filter #(= project-id (:project-id %)))
(group-by :status)
(reduce-kv (fn [m status tasks] (assoc m status (count tasks)))
{:active 0 :completed 0 :canceled 0 :archived 0})))
(defn project-summaries [ctx]
(->> (vals (rmp/project ctx :todo/projects))
(map (fn [project]
(assoc project :task-counts
(project-task-counts ctx (:project-id project)))))))
rmp/project);; By tags (entities)
{:tags #{[:task task-id]}}
;; Multiple tags (batch / scope)
{:tags #{[:user user-id]}}
defreadmodel :ns name {:events ... :version N}{uuid1 {...} uuid2 {...}}assoc for create, update/assoc-in for modify:default method to ignore unknown events:version when reducer behavior changesctx and read via rmp/project (never event-store/read)read-model-schemas in schemas.cljsrc/cjbarre/grain_todo_list/service/todo_list_service/read_models.clj - Task/project/review projectionssrc/cjbarre/grain_todo_list/service/user_service/read_models.clj - User projections