grain-query-handler

Implement query handlers that read from event-sourced read models

SKILL.md

GitHub

Grain Query Handler Pattern

Required Imports

(ns my.service.core.queries
  (:require [ai.obney.grain.event-store-v3.interface :as es]
            [ai.obney.grain.query-processor.interface :refer [defquery]]
            [my.service.interface.read-models :as rm]
            [cognitect.anomalies :as anom]))

Query Template

(defquery :namespace query-name
  "Docstring describing what the query returns."
  [{{:keys [entity-id filter-param]} :query
    :keys [event-store other-dependency]}]

  (let [entity (rm/get-entity event-store entity-id)]
    (if entity
      {:query/result {:entity entity}}
      {::anom/category ::anom/not-found
       ::anom/message "Entity not found"})))

Return Patterns

Success

{:query/result {:entity data
                :related-data more-data}}

Error

{::anom/category ::anom/not-found
 ::anom/message "Human readable message"}

Fat Query Pattern (Screen Queries)

Load all data needed for a UI screen in one query:

(defquery :advisor meetings-screen
  "Fat query for meetings screen - returns all data needed."
  [{{:keys [student-id]} :query
    :keys [event-store]}]

  (let [;; Get primary entity
        student (rm/get-student event-store student-id)
        ;; Get related data
        meetings (rm/get-meetings-for-student event-store student-id)
        ;; Get metadata/options
        meeting-types (rm/get-meeting-types event-store)]

    (if student
      {:query/result {:student student
                      :meetings meetings
                      :meeting-types meeting-types}}
      {::anom/category ::anom/not-found
       ::anom/message "Student not found"})))

List Query with Pagination

(defquery :crm list-contacts
  "List contacts with optional filters and pagination."
  [{{:keys [type-slug status limit offset]} :query
    :keys [event-store]}]

  (let [;; Get all matching contacts
        all-contacts (cond->> (rm/get-contacts-all event-store)
                       type-slug (filter #(= type-slug (:type-slug %)))
                       status (filter #(= status (:status %))))
        total (count all-contacts)
        ;; Apply pagination
        paginated (cond->> all-contacts
                    offset (drop offset)
                    limit (take limit))]

    {:query/result {:contacts (vec paginated)
                    :total total}}))

Batch Loading Pattern

Pre-load related data to avoid N+1 queries:

(defquery :crm list-contacts-with-types
  [{{:keys [type-slug]} :query
    :keys [event-store]}]

  (let [;; Pre-load all contact types (1 query instead of N)
        types-by-id (rm/get-contact-types-by-id event-store)
        ;; Get contacts
        contacts (rm/get-contacts-by-type event-store type-slug)
        ;; Enrich with type data
        enriched (mapv #(assoc % :contact-type (get types-by-id (:type-id %)))
                       contacts)]

    {:query/result {:contacts enriched}}))

Composing Read Models

(defquery :crm get-contact
  "Get contact with attribution in single query."
  [{{:keys [contact-id]} :query
    :keys [event-store]}]

  (let [contact (rm/get-contact event-store contact-id)]
    (if contact
      (let [;; Get related data
            relationships (rm/get-relationships-for-contact event-store contact-id)
            attribution (rm/get-attribution event-store contact-id)]
        {:query/result {:contact (cond-> contact
                                   attribution (assoc :attribution attribution)
                                   (seq relationships) (assoc :relationships relationships))}})
      {::anom/category ::anom/not-found
       ::anom/message "Contact not found"})))

Conventions

  • Queries are read-only (never emit events)
  • Use read models to get current state
  • Return {:query/result {...}} for success
  • Return anomalies for not-found and errors
  • Use fat queries for screen data (reduces round trips)
  • Batch load related data to avoid N+1 queries
  • Include pagination metadata (:total) for list queries

Reference Files

  • components/crm-service/src/ai/obney/workshop/crm_service/core/queries.clj - CRM queries with batch loading