diff --git a/README.md b/README.md index 5554f9c..53614a7 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,20 @@ Each operation expects its implementation to be a function with a ring request o Argo expects as the result of these functions a map with the following keys: * `:data`: The data which will be returned in the API response. This should be a map for a single resource (as implemented with `:get`) or a vector/sequence for `:find`. + - Optional data: `:resource-identifiers` You may _optionally_ include related resource identifier objects. * `:errors`: A map of keywords and string values. Will be converted to the JSON API error format. Works well with Prismatic schema error types. * `:status`: Use to override the status of responses. Defaults to 400 for error responses and 200 for valid responses. * `:exclude-source`: Use this to exclude the source object as per the JSON API spec in error responses. * `:count`: argo provides automatic generation of pagination links if using pagination for `:find`. Use `:count` to let argo know how many total objects exist when implementing pagination. +* `:included` You may _optionally_ include top-level, related resource objects. + - for example: + ```clojure + { + :data {...} + :included {:heroes [{:id 1 :name "Jason"}] + :ally {:id 2 :name "Medea"}} + } + ``` In most circumstances it will probably only be necessary to include either `:data` or `:errors`. @@ -266,11 +276,25 @@ This should return the following reponse. "hero": { "links": { "related": "/v1/achievements/1/hero" - } - } + }, + "data": { "type": "heroes", "id": 1 } + }, }, "type": "achievements" - } + }, + "included": [ + { + "type": "heroes", + "id": "1", + "attributes": { + "created": "2017-02-12T00:09:59Z", + "name": "Jason" + }, + "links": { + "self": "/v1/heroes/1" + } + } + ] } ``` diff --git a/example/src/example/api.clj b/example/src/example/api.clj index bc82293..3972ec3 100644 --- a/example/src/example/api.clj +++ b/example/src/example/api.clj @@ -48,7 +48,8 @@ {:data (db/find-achievements)}) :get (fn [req] - {:data (db/get-achievement (parse-id (-> req :params :id)))}) + {:data (db/get-achievement (parse-id (-> req :params :id))) + :included {:heroes (db/get-achievement-hero (parse-id (-> req :params :id)))}}) :create (fn [req] (if-let [errors (s/check NewAchievement (:body req))] @@ -63,7 +64,7 @@ :rels {:hero {:type :heroes :foreign-key :hero :get (fn [req] - {:data (db/get-achievement-hero (parse-id (-> req :params :id)))})}}}) + {:data (db/get-achievement-hero (parse-id (-> req :params :id)))})}}}) (defapi api {:base-url "/v1" diff --git a/example/src/example/db.clj b/example/src/example/db.clj index faa451a..b3ac119 100644 --- a/example/src/example/db.clj +++ b/example/src/example/db.clj @@ -79,7 +79,7 @@ (defn get-achievement-hero [id] (when (integer? id) - (let [q (str "SELECT heroes.id AS id, heroes.name AS name, heroes.created AS created " + (let [q (str "SELECT heroes.id AS id, heroes.name AS name, heroes.created AS created, heroes.birthplace AS birthplace " "FROM heroes, achievements " "WHERE achievements.id = ? AND heroes.id = achievements.id")] (first (jdbc/query db [q id]))))) diff --git a/src/argo/core.clj b/src/argo/core.clj index e08ba0b..5b58ee6 100644 --- a/src/argo/core.clj +++ b/src/argo/core.clj @@ -34,10 +34,14 @@ (def base-url "") (defn ok - [data & {:keys [status headers links meta]}] + [data & {:keys [status headers links meta included]}] {:status (or status 200) :headers (merge {"Content-Type" "application/vnd.api+json"} headers) - :body (merge {:data data} (when links {:links links}) (when meta {:meta meta}))}) + :body (merge + {:data data} + (when links {:links links}) + (when meta {:meta meta}) + (when included {:included included}))}) (defn flatten-errors ([errors] @@ -92,16 +96,47 @@ (merge (when (> offset 0) {:first (gen-qs uri params-encoded 0 limit)})))))) +(comment (dissoc (apply dissoc x (map (fn [[k v]] (:foreign-key v)) rels)) primary-key)) + +(defn build-relationships + "builds json-api formatted map for relationships, + including resource identifier objecs if in x" + [rel-type x primary-key rels] + (apply merge + (map (fn [[rel-key v]] + {rel-key (merge + {:links {:related (str base-url "/" rel-type "/" (get x primary-key) "/" (name rel-key))}} + (when-let [data (rel-key (:resource-identifiers x))] + {:data data}))}) + rels))) + +(defn remove-unused-keys + [x primary-key rels] + (let [rel-keys (map (fn [[k v]] (:foreign-key v)) rels) + keys-to-remove (concat rel-keys [:resource-identifiers primary-key])] + (apply dissoc x keys-to-remove))) + (defn x-to-api - [type x primary-key & [rels]] + [typ x primary-key & [rels]] (when x - (merge {:type type + (merge {:type typ :id (str (get x primary-key)) - :attributes (dissoc (apply dissoc x (map (fn [[k v]] (:foreign-key v)) rels)) primary-key) - :links {:self (str base-url "/" type "/" (get x primary-key))}} - (when rels {:relationships (apply merge (map (fn [[k v]] - {k {:links {:related (str base-url "/" type "/" (get x primary-key) "/" (name k))}}}) - rels))})))) + :attributes (remove-unused-keys x primary-key rels) + :links {:self (str base-url "/" typ "/" (get x primary-key))}} + (when rels + {:relationships (build-relationships typ x primary-key rels)})))) + +(defn build-included + "builds collection of resources to include in a response" + [included] + (when (not-empty included) + (->> included + (map (fn [[typ included-of-type]] + (when (coll? included-of-type) + (if (map? included-of-type) + [(x-to-api (name typ) included-of-type :id)] + (map (fn [include] (x-to-api (name typ) include :id)) included-of-type))))) + (reduce (fn [acc curr] (into acc curr)))))) (defn wrap-pagination [default-limit max-limit] @@ -217,12 +252,15 @@ exclude-source# :exclude-source status# :status total# :count - m# :meta} (~get-many ~req) + m# :meta + included# :included} (~get-many ~req) pag# (assoc (:page ~req) :count total#) links# (gen-pagination-links ~req pag#)] - (if errors# - (bad-req errors# :status status# :exclude-source exclude-source#) - (ok (map (fn [x#] (x-to-api ~typ x# ~primary-key ~rels)) data#) :links links# :meta m#))))) + (cond + errors# (bad-req errors# :status status# :exclude-source exclude-source#) + (:include (:params ~req)) (bad-req {:?include "include resources not supported"} :status 400 :exclude-source exclude-source#) + (nil? data#) (not-found) + :else (ok (map (fn [x#] (x-to-api ~typ x# ~primary-key ~rels)) data#) :links links# :meta m# :included (build-included included#)))))) ~@(when create `(:post (let [{data# :data @@ -245,11 +283,13 @@ status# :status exclude-source# :exclude-source errors# :errors - m# :meta} (~get-one ~req)] + m# :meta + included# :included} (~get-one ~req)] (cond errors# (bad-req errors# :status status# :exclude-source exclude-source#) + (:include (:params ~req)) (bad-req {:?include "include resources not supported"} :status 400 :exclude-source exclude-source#) (nil? data#) (not-found) - :else (ok (x-to-api ~typ data# ~primary-key ~rels) :meta m#))))) + :else (ok (x-to-api ~typ data# ~primary-key ~rels) :meta m# :included (build-included included#)))))) ~@(when update `(:patch (let [{data# :data @@ -305,7 +345,14 @@ :meta ~m)) `((ok (x-to-api ~typ ~data ~primary-key ~relations) :meta ~m)))))))) ~@(when create - `(:post (rel-req ~create ~req))) + `(:post (let [{data# :data + errors# :errors + exclude-source# :exclude-source + status# :status + m# :meta} (~create ~req)] + (if errors# + (bad-req errors# :status status# :exclude-source exclude-source#) + (ok (x-to-api ~typ data# ~primary-key ~rels) :status 201 :meta m#))))) ~@(when update `(:patch (rel-req ~update ~req)))