Skip to content

Commit

Permalink
SQL-196 Reaction Management API (#321)
Browse files Browse the repository at this point in the history
* SQL-196 Reaction Mgmt API

* format

* SQL-196 minor test refactor

* SQL-196 additional tests

* SQL-196 new json coercion fns for reaction

* SQL-196 deal in camel reaction ids

* SQL-196 API accepts camel everything
  • Loading branch information
milt authored Aug 22, 2023
1 parent dd0f337 commit 4fee1b1
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 16 deletions.
161 changes: 161 additions & 0 deletions src/main/lrsql/admin/interceptors/reaction.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
(ns lrsql.admin.interceptors.reaction
(:require [clojure.spec.alpha :as s]
[io.pedestal.interceptor :refer [interceptor]]
[io.pedestal.interceptor.chain :as chain]
[lrsql.admin.protocol :as adp]
[lrsql.spec.reaction :as rs]
[lrsql.util.reaction :as ru]
[lrsql.util :as u]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Validation Interceptors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def validate-create-reaction-params
"Validate valid params for reaction creation."
(interceptor
{:name ::validate-create-reaction-params
:enter
(fn validate-params [ctx]
(let [{:keys [ruleset] :as raw-params}
(get-in ctx [:request :json-params])
params (cond-> raw-params
ruleset
(update :ruleset ru/json->ruleset))]
(if-some [err (s/explain-data rs/create-reaction-params-spec params)]
;; Invalid parameters - Bad Request
(assoc (chain/terminate ctx)
:response
{:status 400
:body {:error (format "Invalid parameters:\n%s"
(-> err s/explain-out with-out-str))}})
;; Valid params - continue
(assoc ctx ::data params))))}))

(def validate-update-reaction-params
"Validate valid params for reaction update."
(interceptor
{:name ::validate-update-reaction-params
:enter
(fn validate-params [ctx]
(let [{:keys [ruleset] :as raw-params}
(get-in ctx [:request :json-params])
params (-> raw-params
ru/json->input
(update :reaction-id u/str->uuid)
(cond->
ruleset
(update :ruleset ru/json->ruleset)))]
(if-some [err (s/explain-data rs/update-reaction-params-spec params)]
;; Invalid parameters - Bad Request
(assoc (chain/terminate ctx)
:response
{:status 400
:body {:error (format "Invalid parameters:\n%s"
(-> err s/explain-out with-out-str))}})
;; Valid params - continue
(assoc ctx ::data params))))}))

(def validate-delete-reaction-params
"Validate valid params for reaction delete."
(interceptor
{:name ::validate-delete-reaction-params
:enter
(fn validate-params [ctx]
(let [params (-> (get-in ctx [:request :json-params])
ru/json->input
(update :reaction-id u/str->uuid))]
(if-some [err (s/explain-data rs/delete-reaction-params-spec params)]
;; Invalid parameters - Bad Request
(assoc (chain/terminate ctx)
:response
{:status 400
:body {:error (format "Invalid parameters:\n%s"
(-> err s/explain-out with-out-str))}})
;; Valid params - continue
(assoc ctx ::data params))))}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Terminal Interceptors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def create-reaction
"Create a new reaction and store it."
(interceptor
{:name ::create-reaction
:enter
(fn create-reaction [ctx]
(let [{lrs :com.yetanalytics/lrs
{:keys [ruleset active]} ::data}
ctx
{:keys [result]}
(adp/-create-reaction lrs ruleset active)]
(assoc ctx
:response
{:status 200 :body {:reactionId result}})))}))

(def get-all-reactions
"List all reactions."
(interceptor
{:name ::get-all-reactions
:enter
(fn get-all-reactions [ctx]
(let [{lrs :com.yetanalytics/lrs} ctx
result
(adp/-get-all-reactions lrs)]
(assoc ctx
:response
{:status 200 :body {:reactions
(map
(fn [reaction-record]
(-> reaction-record
(update :created u/time->str)
(update :modified u/time->str)
(update :ruleset ru/ruleset->json)))
result)}})))}))

(def update-reaction
"Update an existing reaction."
(interceptor
{:name ::update-reaction
:enter
(fn create-reaction [ctx]
(let [{lrs :com.yetanalytics/lrs
{:keys [reaction-id ruleset active]} ::data}
ctx
{:keys [result]}
(adp/-update-reaction lrs reaction-id ruleset active)]
(cond
(uuid? result)
(assoc ctx
:response
{:status 200 :body {:reactionId result}})
(= :lrsql.reaction/reaction-not-found-error result)
(assoc (chain/terminate ctx)
:response
{:status 404
:body {:error (format "The reaction \"%s\" does not exist!"
(u/uuid->str reaction-id))}}))))}))

(def delete-reaction
"Delete a reaction."
(interceptor
{:name ::delete-reaction
:enter
(fn delete-reaction [ctx]
(let [{lrs :com.yetanalytics/lrs
{:keys [reaction-id]} ::data}
ctx
{:keys [result]}
(adp/-delete-reaction lrs reaction-id)]
(cond
(uuid? result)
(assoc ctx
:response
{:status 200 :body {:reactionId result}})
(= :lrsql.reaction/reaction-not-found-error result)
(assoc (chain/terminate ctx)
:response
{:status 404
:body {:error (format "The reaction \"%s\" does not exist!"
(u/uuid->str reaction-id))}}))))}))
37 changes: 36 additions & 1 deletion src/main/lrsql/admin/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[lrsql.admin.interceptors.ui :as ui]
[lrsql.admin.interceptors.jwt :as ji]
[lrsql.admin.interceptors.status :as si]
[lrsql.admin.interceptors.reaction :as ri]
[lrsql.util.interceptor :as util-i]))

(defn- make-common-interceptors
Expand Down Expand Up @@ -112,6 +113,36 @@
(ui/get-env inject-config))
:route-name :lrsql.admin.ui/get-env]})

(defn admin-reaction-routes
[common-interceptors jwt-secret jwt-leeway]
#{;; Create a reaction
["/admin/reaction" :post (conj common-interceptors
ri/validate-create-reaction-params
(ji/validate-jwt jwt-secret jwt-leeway)
ji/validate-jwt-account
ri/create-reaction)
:route-name :lrsql.admin.reaction/post]
;; Get all reactions
["/admin/reaction" :get (conj common-interceptors
(ji/validate-jwt jwt-secret jwt-leeway)
ji/validate-jwt-account
ri/get-all-reactions)
:route-name :lrsql.admin.reaction/get]
;; Update a reaction
["/admin/reaction" :put (conj common-interceptors
ri/validate-update-reaction-params
(ji/validate-jwt jwt-secret jwt-leeway)
ji/validate-jwt-account
ri/update-reaction)
:route-name :lrsql.admin.reaction/put]
;; Delete a reaction
["/admin/reaction" :delete (conj common-interceptors
ri/validate-delete-reaction-params
(ji/validate-jwt jwt-secret jwt-leeway)
ji/validate-jwt-account
ri/delete-reaction)
:route-name :lrsql.admin.reaction/delete]})

(defn add-admin-routes
"Given a set of routes `routes` for a default LRS implementation,
add additional routes specific to creating and updating admin
Expand All @@ -123,6 +154,7 @@
enable-admin-ui
enable-admin-status
enable-account-routes
enable-reaction-routes
oidc-interceptors
oidc-ui-interceptors]
:or {oidc-interceptors []
Expand All @@ -143,4 +175,7 @@
{:enable-admin-status enable-admin-status}))
(when enable-admin-status
(admin-status-routes
common-interceptors-oidc secret leeway)))))
common-interceptors-oidc secret leeway))
(when enable-reaction-routes
(admin-reaction-routes
common-interceptors secret leeway)))))
16 changes: 16 additions & 0 deletions src/main/lrsql/spec/reaction.clj
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,19 @@

(def error-reaction-ret-spec
(s/keys :req-un [:lrsql.spec.reaction.error-reaction/result]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Params
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def create-reaction-params-spec
(s/keys :req-un [::ruleset
::active]))

(def update-reaction-params-spec
(s/keys :req-un [::reaction-id
(or ::ruleset
::active)]))

(def delete-reaction-params-spec
(s/keys :req-un [::reaction-id]))
28 changes: 16 additions & 12 deletions src/main/lrsql/system/webserver.clj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
(oidc/init
config
(:config lrs))
;; LRS reaction toggle
{:keys [enable-reactions]}
(:config lrs)
;; Make routes - the lrs error interceptor is appended to the
;; start to all lrs routes
routes
Expand All @@ -54,15 +57,16 @@
(handle-json-parse-exn)]
oidc-resource-interceptors)})
(add-admin-routes
{:lrs lrs
:exp jwt-exp
:leeway jwt-lwy
:secret private-key
:enable-admin-ui enable-admin-ui
:enable-admin-status enable-admin-status
:enable-account-routes enable-local-admin
:oidc-interceptors oidc-admin-interceptors
:oidc-ui-interceptors oidc-admin-ui-interceptors}))
{:lrs lrs
:exp jwt-exp
:leeway jwt-lwy
:secret private-key
:enable-admin-ui enable-admin-ui
:enable-admin-status enable-admin-status
:enable-account-routes enable-local-admin
:enable-reaction-routes enable-reactions
:oidc-interceptors oidc-admin-interceptors
:oidc-ui-interceptors oidc-admin-ui-interceptors}))
;; Build allowed-origins list. Add without ports as well for
;; default ports
allowed-list
Expand Down Expand Up @@ -118,8 +122,8 @@
http/start)]
;; Logging
(let [{{ssl-port :ssl-port} ::http/container-options
http-port ::http/port
host ::http/host} service]
http-port ::http/port
host ::http/host} service]
(if http-port
(log/infof "Starting new webserver at host %s, HTTP port %s, and SSL port %s"
host
Expand All @@ -135,7 +139,7 @@
:service service
:server server))
(throw (ex-info "LRS Required to build service!"
{:type ::start-no-lrs
{:type ::start-no-lrs
:webserver this})))))
(stop
[this]
Expand Down
37 changes: 36 additions & 1 deletion src/main/lrsql/util/reaction.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
[lrsql.spec.common :as cs]
[lrsql.spec.reaction :as rs]
[lrsql.spec.statement :as ss]
[xapi-schema.spec :as xs]))
[xapi-schema.spec :as xs]
[camel-snake-kebab.core :as csk]
[camel-snake-kebab.extras :as cske]))

(s/fdef path->string
:args (s/cat :path ::rs/path
Expand Down Expand Up @@ -94,3 +96,36 @@
"On read, the reaction template has keyword keys. Stringify them!"
[raw-ruleset]
(update raw-ruleset :template walk/stringify-keys))

(s/fdef json->ruleset
:args (s/cat :raw-ruleset ::cs/any-json)
:ret ::cs/any-json)

(defn json->ruleset
"Pre-validation, read in the ruleset from JSON, coercing keys from camel to
kebab and ensuring string keys in the template."
[{:keys [template] :as raw-ruleset}]
(cond-> (cske/transform-keys
csk/->kebab-case-keyword
(dissoc raw-ruleset :template))
template (assoc :template (walk/stringify-keys template))))

(s/fdef ruleset->json
:args (s/cat :edn ::rs/ruleset)
:ret ::cs/any-json)

(defn ruleset->json
"Prepare ruleset for JSON response by camelizing keys but leaving template
untouched."
[{:keys [template] :as ruleset}]
(assoc (cske/transform-keys
csk/->camelCaseKeyword
ruleset)
:template template))

(defn json->input
"Where an input contains a camel id, kebab it"
[{:keys [reactionId] :as input}]
(-> input
(dissoc :reactionId)
(assoc :reaction-id reactionId)))
Loading

0 comments on commit 4fee1b1

Please sign in to comment.