diff --git a/src/sandbar/tables.clj b/src/sandbar/tables.clj deleted file mode 100644 index 59c2054..0000000 --- a/src/sandbar/tables.clj +++ /dev/null @@ -1,471 +0,0 @@ -;; Copyright (c) Brenton Ashworth. All rights reserved. -;; The use and distribution terms for this software are covered by the -;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) -;; which can be found in the file COPYING at the root of this distribution. -;; By using this software in any fashion, you are agreeing to be bound by -;; the terms of this license. -;; You must not remove this notice, or any other, from this software. - -(ns sandbar.tables - "HTML tables which may be filtered, sorted and paged." - (:use [clojure.contrib.json :only [json-str]] - [ring.util.codec :only [url-encode]] - [hiccup.page-helpers :only [include-js]] - [sandbar.core :only [get-param link-to-js image cpath]] - [sandbar.stateful-session :only [update-session! - session-get]]) - (:require [clojure.string :as str])) - -(declare *table-id*) - -(defprotocol ResourceList - (find-resources [this filters page-and-sort]) - (fields [this])) - -(defprotocol PagedResources - (page-size [this]) - (total-resource-count [this filters])) - -(defprotocol Labels - (label [this key])) - -(defn merge-table-state-vecs [old new] - (if (number? old) - new - (let [order (distinct - (concat (map first (partition 2 old)) - (map first (partition 2 new)))) - m (reduce (fn [a b] - (assoc a (first b) (last b))) - {} - (partition 2 (concat old new)))] - (vec - (apply concat - (filter #(not (= :remove (last %))) - (map #(vector % (m %)) order))))))) - -(defn update-table-state! [params] - (-> (update-session! - (fn [a b] - (let [current-state (or (-> a :table-state *table-id*) {})] - (-> a - (assoc-in [:table-state *table-id*] - (merge-with merge-table-state-vecs - current-state - b))))) - (let [s - (hash-map :sort - (vec (concat - (if-let [sort (get-param params :sort-asc)] - [(keyword sort) :asc]) - (if-let [sort (get-param params :sort-desc)] - [(keyword sort) :desc]) - (if-let [sort (get-param params :remove-sort)] - [(keyword sort) :remove])))) - f - (hash-map :filter - (vec (concat - (if-let [filter (get-param params :filter)] - [(keyword filter) - (get-param params :filter-value)]) - (if-let [filter (get-param params :remove-filter)] - [(keyword filter) :remove])))) - p - (cond (or (not (empty? (concat (:filter f) (:sort s))))) - {:page 0} - :else (if-let [page (get-param params :page)] - {:page (Integer/valueOf page)}))] - (merge s f p))) - :table-state *table-id*)) - -(defn build-page-and-sort-map [adapter table-state-map] - (let [pas (assoc {} :sort - (vec - (apply concat - (map #(list (last %) (name (first %))) - (partition 2 (:sort table-state-map))))))] - (if (satisfies? PagedResources adapter) - (assoc pas :page (or (:page table-state-map) 0) - :page-size (page-size adapter)) - pas))) - -(defn build-filter-map [table-state-map] - (reduce (fn [a b] (assoc a (first b) (last b))) - {} - (partition 2 (:filter table-state-map)))) - -(defn current-filters! [params] - (let [t-state (update-table-state! params)] - (build-filter-map t-state))) - -(defn current-page-and-sort! [adapter params] - (let [t-state (update-table-state! params)] - (build-page-and-sort-map adapter t-state))) - -(defn get-column-name [column-spec-row] - (if (keyword? column-spec-row) - column-spec-row - (:column column-spec-row))) - -(defn table-cell [map-or-value & values] - (vec - (filter #(not (nil? %)) - (if (map? map-or-value) - [:td (:attr map-or-value) - (if (contains? (:actions map-or-value) :filter) - (link-to-js (addFilter (name (:column map-or-value)) - (url-encode (:value map-or-value))) - (:value map-or-value) - *table-id*) - (:value map-or-value))] - (if (seq values) - (vec (concat [:td map-or-value] values)) - [:td map-or-value]))))) - -(defn table-row - ([coll] [:tr (doall (map table-cell coll))]) - ([coll row-class] - [:tr {:class row-class} (doall (map table-cell coll))])) - -(defn table-header [coll] - [:tr (map #(vector :th {:nowrap ""} %) coll)]) - -(defn standard-table [props columns column-fn data] - [:table {:class "list"} - (table-header (map #(if-let [p (props %)] p %) columns)) - (map - (fn [row-data class] - (table-row (doall (map #(column-fn % row-data) columns)) class)) - data (cycle ["odd" "even"]))]) - -(defn get-table-state [table-name] - (session-get [:table-state table-name])) - -(defn opposite-sort-dir [d] - (cond (= d :asc) :desc - (= d :desc) :asc - :else d)) - -(defn table-column-names [column-spec] - (map get-column-name column-spec)) - -(defn table-sort-columns [column-spec] - (set - (map #(if (contains? (:actions %) :sort) (:column %)) - (filter map? column-spec)))) - -(defn sort-table-header [adapter column-spec] - (let [t-state (:sort (get-table-state *table-id*)) - sort-dir-map (reduce - (fn [a b] - (assoc a (first b) (last b))) - {} - (partition 2 t-state)) - sort-columns (table-sort-columns column-spec)] - [:tr (doall - (map - #(let [sort-dir (sort-dir-map %) - opp-sort-dir (name (if-let [sd (opposite-sort-dir sort-dir)] - sd - :asc))] - (vector :th {:nowrap ""} - (if (contains? sort-columns %) - (link-to-js (sortColumn opp-sort-dir (name %)) - (label adapter %) - *table-id*) - (label adapter %)) - " " - (cond (= sort-dir :asc) (image "sort_ascending.png") - (= sort-dir :desc) (image "sort_descending.png") - :else (image "blank16.gif")))) - (table-column-names column-spec)))])) - -(defn- create-saf-table-control [t-state k title link-fn data-fn] - (let [t-state (k t-state)] - (if (seq t-state) - (vec - (concat - [:div title] - (interpose - ", " - (apply vector (map link-fn (data-fn t-state)))))) - ""))) - -(defn create-table-sort-and-filter-controls [adapter] - (let [current-state (get-table-state *table-id*)] - (vec - (conj - [:div {:class "filter-and-sort-controls"}] - (create-saf-table-control current-state :sort "Remove sort: " - #(link-to-js (removeSort (name %)) - (label adapter (keyword %)) - *table-id*) - #(map first (partition 2 %))) - (create-saf-table-control current-state :filter "Remove filter: " - #(let [c (first %)] - (link-to-js (removeFilter (name c)) - (str - (label adapter (keyword c)) - " = " - (last %)) - *table-id*)) - #(partition 2 %)))))) - -(defn- page-controls [view content] - (if-let [page-size (:page-size view)] - (let [{:keys [last page available column-count]} view - next-page (+ page 1) - previous-page (- page 1)] - [:tr - [:td {:colspan column-count} - [:table {:width "100%" :cellspacing "0" :cellpadding "0" :border "0"} - [:tr - [:td {:width "40px" :align "left"} - (if (>= previous-page 0) - (link-to-js (page previous-page) - "back" - *table-id*))] - content - [:td {:align "right"} - (if (< last available) - (link-to-js (page next-page) - "next" - *table-id*))]]]]]))) - -(defn- page-control-header [name view] - (page-controls view - (if (:page-size view) - (let [{:keys [first last available]} view] - [:td.filter-table-summary - "showing " - name " " - [:b (+ first 1)] - " through " - [:b last] - " of " - [:b available]])))) - -(defn- page-control-footer [view] - (page-controls view [:td])) - -(defn make-table-view - "Create the current view of the data that will be displayed in the table. - This includes paging information." - [adapter column-spec params] - (let [page-and-sort (current-page-and-sort! adapter params) - filters (current-filters! params) - table-data (find-resources adapter filters page-and-sort) - view {:data table-data - :column-count (count column-spec)}] - (if-let [page-size (:page-size page-and-sort)] - (let [page (get page-and-sort :page 0) - first (* page page-size) - visible (count table-data) - last (+ visible first)] - (merge view {:page-size page-size - :page page - :first first - :last last - :available (total-resource-count adapter filters) - :visible visible})) - view))) - -(defmulti display-table-cell (fn [type k data] [type k])) - -(defmethod display-table-cell :default [type k data] - (or (k data) "")) - -(defn build-row - "Build one table row." - [adapter column-spec row-data css-class] - (let [next-row (table-row - (map #(let [cell-data - (display-table-cell (:type adapter) - (get-column-name %) - row-data) - cell-data (if (map? cell-data) - cell-data - {:value cell-data})] - (merge - {:column (get-column-name %) - :value nil - :attr (merge {:align :left} - (:attr %)) - :actions (:actions %)} - cell-data)) - column-spec) - css-class)] - next-row)) - -(defn filter-and-sort-table [adapter column-spec params] - (binding [*table-id* (keyword (str (name (:type adapter)) "-table"))] - (let [table-view (make-table-view adapter column-spec params)] - [:div {:id *table-id* :class (or (:class adapter) - "filter-and-sort-table")} - (create-table-sort-and-filter-controls adapter) - [:table {:class "list"} - (page-control-header (label adapter *table-id*) - table-view) - (sort-table-header adapter column-spec) - (doall - (map (partial build-row adapter column-spec) - (:data table-view) - (cycle ["odd" "even"]))) - (page-control-footer table-view)] - (include-js (str (cpath "/js/sandbar/table/") - (name *table-id*) - ".js"))]))) - -(defn table-as-json [table] - {:status 200 - :headers {"Content-Type" "application/json"} - :body (json-str {:html table})}) - -;; Use Scriptjure for generating javascript. - -(defmulti js-ajax (fn [js-lib _ _] js-lib)) - -(defmethod js-ajax :prototype [js-lib qualifier table-id] - (str " -function updateTable_" qualifier "(uri) { - new Ajax.Request(uri, { - onSuccess: function(response) { - var data = response.responseJSON; - displayResults_" qualifier "(data); - } - }); -} - -function displayResults_" qualifier "(data) { - $('" table-id "').replace(data['html']); -}")) - -(defmethod js-ajax :jquery [js-lib qualifier table-id] - (str " -function updateTable_" qualifier "(uri) { - $.ajax({ - type: 'post', - dataType: 'json', - url: uri, - success: function(data) { - $('#" table-id "').html(data['html']); - } - }); -}")) - -(defn js [table-id table-uri js-lib] - (let [q (.replaceAll table-id "-" "_")] - (str " -function page_" q "(n) { - updateTable_" q "('" table-uri "?page' + '=' + n); -} - -function sortColumn_" q "(dir, column) { - updateTable_" q "('" table-uri "?sort-' + dir + '=' + column); -} - -function removeSort_" q "(column) { - updateTable_" q "('" table-uri "?remove-sort=' + column); -} - -function addFilter_" q "(column, value) { - updateTable_" q "('" table-uri "?filter=' + column + '&filter-value=' + value); -} - -function removeFilter_" q "(column) { - updateTable_" q "('" table-uri "?remove-filter=' + column); -}" "\n" (js-ajax js-lib q table-id)))) - -(defn wrap-table-js - [handler js-uri-map js-lib] - (fn [request] - (let [uri (:uri request)] - (if (.startsWith uri "/js/sandbar/table/") - (let [table-id (.substring uri 18 (- (count uri) 3))] - {:status 200 - :headers {"Content-Type" "text/javascript"} - :body (js table-id ((keyword table-id) js-uri-map) js-lib)}) - (handler request))))) - -;; -;; Functions for adapting this table to carte backend -;; - -(defn remove-path-from-keyword - ([k] - (keyword - (let [s (str/split (name k) #"[.]")] - (if (> (count s) 2) - (->> s - (drop (- (count s) 2)) - (interpose ".") - (apply str)) - k)))) - ([path k] - (keyword (subs (name k) - (if (= path "") 0 (+ 1 (count path))))))) - -(defn filters-on-path [path filters] - (reduce (fn [a b] - (let [k (remove-path-from-keyword path (key b))] - (if (= (.indexOf (name k) ".") -1) - (assoc a k (val b)) - a))) - {} - (select-keys filters - (filter (if (= path "") - #(= (.indexOf (name %) ".") -1) - #(.startsWith (name %) (str path "."))) - (keys filters))))) - -(defn- new-root-path [path next] - (let [n (name next)] - (if (= path "") - n - (str path - "." - n)))) - -(defn carte-query-sort [sort] - (if (and sort - (not (empty? sort))) - (map #(let [[field dir] (reverse %)] - [(remove-path-from-keyword (keyword field)) dir]) - (partition 2 sort)) - nil)) - -(defn carte-query - ([table filters] - (let [query [table] - query (if (empty? filters) query (conj query filters))] - query)) - ([root-path table joins filters] - (let [query (carte-query table - (filters-on-path root-path filters)) - join-queries (map #(if (coll? %) - (carte-query (new-root-path root-path (first %)) - (first %) - (rest %) - filters) - (let [p (new-root-path root-path %)] - (carte-query % - (filters-on-path p filters)))) - joins)] - (if (not (empty? join-queries)) - (concat query [:with] join-queries) - query)))) - -(defn carte-table-adapter - "Transform filter and sort information from a filter-and-sort table into - a query that carte can understand." - [table filters sort-and-page] - (let [{:keys [sort page page-size]} sort-and-page - tables (if (keyword? table) [table] table) - query (carte-query "" (first tables) (rest tables) filters) - order-by (carte-query-sort sort) - query (vec (if order-by - (concat query [:order-by] order-by) - query))] - (if page-size - (concat query [:page page page-size]) - query))) diff --git a/test/sandbar/test/tables.clj b/test/sandbar/test/tables.clj deleted file mode 100644 index 8ffdae9..0000000 --- a/test/sandbar/test/tables.clj +++ /dev/null @@ -1,275 +0,0 @@ -;; Copyright (c) Brenton Ashworth. All rights reserved. -;; The use and distribution terms for this software are covered by the -;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) -;; which can be found in the file COPYING at the root of this distribution. -;; By using this software in any fashion, you are agreeing to be bound by -;; the terms of this license. -;; You must not remove this notice, or any other, from this software. - -(ns sandbar.test.tables - (:use [clojure.test :only [deftest testing is]] - [sandbar.tables] - [sandbar.stateful-session :only [sandbar-session]])) - -(deftest test-merge-table-state-vecs - (testing "merge table sort state" - (testing "adding a new sort to empty list" - (is (= (merge-table-state-vecs [] [:b :asc]) - [:b :asc]))) - (testing "adding a new sort" - (is (= (merge-table-state-vecs [:a :asc] [:b :asc]) - [:a :asc :b :asc]))) - (testing "adding a new sort maintaining the correc order" - (is (= (merge-table-state-vecs [:b :asc] [:a :asc]) - [:b :asc :a :asc]))) - (testing "updating the sort direction" - (is (= (merge-table-state-vecs [:b :asc :a :desc] [:b :desc]) - [:b :desc :a :desc]))) - (testing "removing the last sort" - (is (= (merge-table-state-vecs [:b :asc :a :desc] [:a :remove]) - [:b :asc]))) - (testing "removing the first sort" - (is (= (merge-table-state-vecs [:b :asc :a :desc] [:b :remove]) - [:a :desc])))) - (testing "merge table filter state" - (testing "adding a new filter to empty list" - (is (= (merge-table-state-vecs [] [:b "a"]) - [:b "a"]))) - (testing "adding a new filter" - (is (= (merge-table-state-vecs [:a "a"] [:b "b"]) - [:a "a" :b "b"]))) - (testing "adding a new filter maintaining the correc order" - (is (= (merge-table-state-vecs [:b "b"] [:a "a"]) - [:b "b" :a "a"]))) - (testing "updating a filter" - (is (= (merge-table-state-vecs [:b "b" :a "a"] [:b "c"]) - [:b "c" :a "a"]))) - (testing "removing the last filter" - (is (= (merge-table-state-vecs [:b "b" :a "a"] [:a :remove]) - [:b "b"]))) - (testing "removing the first filter" - (is (= (merge-table-state-vecs [:b "b" :a "a"] [:b :remove]) - [:a "a"]))))) - -(defn test-table-state [s] - {:table-state {:test-table s}}) - -(deftest test-update-table-state! - (binding [*table-id* :test-table] - (testing "update table state sort state" - (testing "when adding sort to initially empty state" - (binding [sandbar-session (atom {})] - (is (= (update-table-state! {"sort-asc" "a"}) - {:sort [:a :asc] :filter [] :page 0})))) - (testing "when changing the direction of an existing sort" - (binding [sandbar-session - (atom (test-table-state {:sort [:a :asc]}))] - (is (= (update-table-state! {"sort-desc" "a"}) - {:sort [:a :desc] :filter [] :page 0})))) - (testing "when adding multiple sorts at the same time" - (binding [sandbar-session - (atom (test-table-state {:sort [:a :asc]}))] - (is (= (update-table-state! - {"sort-asc" "b" "sort-desc" "c"}) - {:sort [:a :asc :b :asc :c :desc] :filter [] :page 0})))) - (testing "when adding a new sort to an existing sort" - (binding [sandbar-session - (atom (test-table-state {:sort [:b :asc]}))] - (is (= (update-table-state! {"sort-desc" "a"}) - {:sort [:b :asc :a :desc] :filter [] :page 0})))) - (testing "when removing an existing sort" - (binding [sandbar-session (atom (test-table-state - {:sort [:b :asc :a :asc]}))] - (is (= (update-table-state! {"remove-sort" "a"}) - {:sort [:b :asc] :filter [] :page 0}))))) - (testing "update table filter state" - (testing "when adding filter to initially empty state" - (binding [sandbar-session (atom {})] - (is (= (update-table-state! - {"filter" "a" "filter-value" "v-a"}) - {:sort [] :filter [:a "v-a"] :page 0})))) - (testing "when changing the value of a filter" - (binding [sandbar-session - (atom (test-table-state {:filter [:a "v-a"]}))] - (is (= (update-table-state! - {"filter" "a" "filter-value" "v-b"}) - {:sort [] :filter [:a "v-b"] :page 0})))) - (testing "when adding a new filter to an existing filter" - (binding [sandbar-session - (atom (test-table-state {:filter [:b "v-b"]}))] - (is (= (update-table-state! - {"filter" "a" "filter-value" "v-a"}) - {:sort [] :filter [:b "v-b" :a "v-a"] :page 0})))) - (testing "when removing an existing filter" - (binding [sandbar-session (atom (test-table-state - {:filter [:b "v-b" :a "v-a"]}))] - (is (= (update-table-state! {"remove-filter" "a"}) - {:sort [] :filter [:b "v-b"] :page 0}))))))) - -(deftest test-build-page-and-sort-map - (is (= (build-page-and-sort-map nil {:sort [:a :asc :b :desc]}) - {:sort [:asc "a" :desc "b"]}))) - -(deftest test-build-filter-map - (is (= (build-filter-map {:filter [:a "v-a" :b "v-b"]}) - {:a "v-a" :b "v-b"}))) - -(defrecord TestTable [page-size] - PagedResources - (page-size [this] page-size) - (total-resource-count [this filter] 0) - - Labels - (label [this key] key)) - -(deftest test-current-page-and-sort! - (binding [*table-id* :test-table - sandbar-session (atom (test-table-state {:sort [:b :asc]}))] - (is (= (current-page-and-sort! {} {"sort-desc" "a"}) - {:sort [:asc "b" :desc "a"]})) - (is (= (current-page-and-sort! (TestTable. 10) {"sort-desc" "a"}) - {:sort [:asc "b" :desc "a"] :page 0 :page-size 10})))) - -(deftest test-create-table-sort-and-filter-controls - (binding [*table-id* :test-table - sandbar-session (atom (test-table-state {:sort [:a :asc] - :filter [:b "v-b"]}) )] - (is (= (create-table-sort-and-filter-controls (TestTable. 0)) - [:div {:class "filter-and-sort-controls"} - [:div "Remove sort: " - [:a {:href "javascript:removeSort_test_table('a');"} [:a]]] - [:div "Remove filter: " - [:a {:href "javascript:removeFilter_test_table('b');"} - [":b = v-b"]]]])))) - -(deftest test-table-cell - (binding [*table-id* :a] - (testing "create table cell" - (testing "with just a value" - (is (= (table-cell "v") - [:td "v"]))) - (testing "with a value and attributes" - (is (= (table-cell {:attr {:class "c"} :value "v"}) - [:td {:class "c"} "v"]))) - (testing "with a nil value" - (is (= (table-cell nil) - [:td]))) - (testing "with a filter" - (is (= (table-cell {:value "v" - :actions #{:filter} :column :x}) - [:td [:a {:href "javascript:addFilter_a('x', 'v');"} ["v"]]]))) - (testing "with a filter and attributes" - (is (= (table-cell {:value "v" :attr {:class "c"} - :actions #{:filter} :column :x}) - [:td {:class "c"} - [:a {:href "javascript:addFilter_a('x', 'v');"} ["v"]]]))) - (testing "with multiple values" - (is (= (table-cell "v1" "v2" "v3") - [:td "v1" "v2" "v3"]))) - (testing "with multiple values one of which is nil" - (is (= (table-cell "v1" nil "v3") - [:td "v1" "v3"])))))) - -(def table-javascript - " -function page_test_table(n) { - updateTable_test_table('/ideas?page' + '=' + n); -} - -function sortColumn_test_table(dir, column) { - updateTable_test_table('/ideas?sort-' + dir + '=' + column); -} - -function removeSort_test_table(column) { - updateTable_test_table('/ideas?remove-sort=' + column); -} - -function addFilter_test_table(column, value) { - updateTable_test_table('/ideas?filter=' + column + '&filter-value=' + value); -} - -function removeFilter_test_table(column) { - updateTable_test_table('/ideas?remove-filter=' + column); -} - -function updateTable_test_table(uri) { - new Ajax.Request(uri, { - onSuccess: function(response) { - var data = response.responseJSON; - displayResults_test_table(data); - } - }); -} - -function displayResults_test_table(data) { - $('test-table').replace(data['html']); -}") - -(deftest test-js - (is (= (js "test-table" "/ideas" :prototype) - table-javascript))) - -(deftest test-filters-on-path - (is (= (filters-on-path "" {:name "A" :person.name "B"}) - {:name "A"})) - (is (= (filters-on-path "person" {:name "A" :person.name "B"}) - {:name "B"})) - (is (= (filters-on-path "person" {:name "A" :person.name "B" - :person.mom.name "C"}) - {:name "B"})) - (is (= (filters-on-path "person.mom" {:name "A" :person.name "B" - :person.mom.name "C"}) - {:name "C"}))) - -(deftest test-carte-table-adapter - (is (= (carte-table-adapter :artist {:name "Jim"} {}) - [:artist {:name "Jim"}])) - (is (= (carte-table-adapter :artist {:name "Jim"} {:sort [:asc :name] - :page 10 - :page-size 5}) - [:artist {:name "Jim"} :order-by [:name :asc] :page 10 5])) - (is (= (carte-table-adapter [:artist :albums] - {:name "Jim" :albums.title "Fun"} - {:sort [:asc :name] :page 10 :page-size 5}) - [:artist {:name "Jim"} - :with [:albums {:title "Fun"}] - :order-by [:name :asc] - :page 10 5])) - (is (= (carte-table-adapter [:artist :albums] - {:name "Jim" :albums.title "Fun"} - {:sort [:asc :name :desc :albums.title] - :page 10 :page-size 5}) - [:artist {:name "Jim"} - :with [:albums {:title "Fun"}] - :order-by [:name :asc] [:albums.title :desc] - :page 10 5])) - (is (= (carte-table-adapter [:artist :albums :genre] - {:name "Jim" :albums.title "Fun" - :genre.name "Rock"} - {:sort [:asc :name :desc :albums.title] - :page 10 :page-size 5}) - [:artist {:name "Jim"} - :with [:albums {:title "Fun"}] [:genre {:name "Rock"}] - :order-by [:name :asc] [:albums.title :desc] - :page 10 5])) - (is (= (carte-table-adapter [:artist [:albums :genre]] - {:name "Jim" :albums.title "Fun" - :albums.genre.name "Rock"} - {:sort [:asc :name :desc :albums.title - :asc :albums.genre.name] - :page 10 :page-size 5}) - [:artist {:name "Jim"} - :with [:albums {:title "Fun"} :with [:genre {:name "Rock"}]] - :order-by [:name :asc] [:albums.title :desc] [:genre.name :asc] - :page 10 5])) - (is (= (carte-table-adapter [:artist [:albums :genre] :genre] - {:name "Jim" :albums.title "Fun" - :albums.genre.name "Rock" :genre.name "Rock"} - {:sort [:asc :name :desc :albums.title - :asc :albums.genre.name] - :page 10 :page-size 5}) - [:artist {:name "Jim"} - :with [:albums {:title "Fun"} :with [:genre {:name "Rock"}]] - [:genre {:name "Rock"}] - :order-by [:name :asc] [:albums.title :desc] [:genre.name :asc] - :page 10 5])))