diff --git a/src/app/cljs/one/repmax/datastore_configuration/observer.cljs b/src/app/cljs/one/repmax/datastore_configuration/observer.cljs index 487dea0..b2ab65f 100644 --- a/src/app/cljs/one/repmax/datastore_configuration/observer.cljs +++ b/src/app/cljs/one/repmax/datastore_configuration/observer.cljs @@ -8,24 +8,25 @@ (defmulti do-initialization-step (fn [& args] (first args))) -(defmethod do-initialization-step :verify-credentials [_ api-key] - (mongo/find-databases api-key +(defmethod do-initialization-step :verify-credentials [_ config] + (mongo/find-databases config #(do - (cookies/set-cookie :api-key api-key) - (dispatch/fire :action {:action :datastore-configuration/credentials-verified, :api-key api-key})) - #(initialization-error-callback api-key %))) - -(defmethod do-initialization-step :verify-database [_ api-key] - (mongo/find-database api-key - #(dispatch/fire :action {:action :datastore-configuration/database-verified, :api-key api-key}) - #(initialization-error-callback api-key %))) - -(defmethod do-initialization-step :verify-collections [_ api-key] - (mongo/find-collections api-key - #(find-collections-success-callback api-key %) - #(initialization-error-callback api-key %))) - -(defmethod do-initialization-step :ready [_ api-key] + (cookies/set-cookie :api-key (:api-key config)) + (cookies/set-cookie :database (:database config)) + (dispatch/fire :action {:action :datastore-configuration/credentials-verified, :configuration config})) + #(initialization-error-callback config %))) + +(defmethod do-initialization-step :verify-database [_ config] + (mongo/find-database config + #(dispatch/fire :action {:action :datastore-configuration/database-verified, :configuration config}) + #(initialization-error-callback config %))) + +(defmethod do-initialization-step :verify-collections [_ config] + (mongo/find-collections config + #(find-collections-success-callback config %) + #(initialization-error-callback config %))) + +(defmethod do-initialization-step :ready [_ _] (dispatch/fire :action {:action :datastore-configuration/ready})) ;; No-op. No action necessary when we transition to the :initialization-failed state. @@ -33,19 +34,19 @@ ;; TODO Enhance function to recurse through the list of necessary collections ;; and create each one. Fire action :datastore-configuration/collections-verified. -(defn find-collections-success-callback [api-key collections] +(defn find-collections-success-callback [config collections] (if (contains-collection? collections "exercises") ;; TODO Get collection name from elsewhere - (dispatch/fire :action {:action :datastore-configuration/collections-verified, :api-key api-key}) - (mongo/create-collection api-key "exercises" ;; TODO Get collection name from elsewhere - #(dispatch/fire :action {:action :datastore-configuration/collections-verified, :api-key api-key}) - #(dispatch/fire :action {:action :datastore-configuration/initialization-failed, :api-key api-key})))) + (dispatch/fire :action {:action :datastore-configuration/collections-verified, :configuration config}) + (mongo/create-collection config "exercises" ;; TODO Get collection name from elsewhere + #(dispatch/fire :action {:action :datastore-configuration/collections-verified, :configuration config}) + #(dispatch/fire :action {:action :datastore-configuration/initialization-failed, :configuration config})))) (defn contains-collection? [collections collection-name] (some #(= collection-name (% "name")) collections)) -(defn initialization-error-callback [api-key response] +(defn initialization-error-callback [config response] (dispatch/fire :action {:action :datastore-configuration/initialization-failed - :api-key api-key + :configuration config :error response})) ;;; Register reactors @@ -55,4 +56,5 @@ (let [old-config-state (-> event :old :datastore-configuration :state) new-config-state (-> event :new :datastore-configuration :state)] (if-not (= old-config-state new-config-state) - (do-initialization-step new-config-state (-> event :new :datastore-configuration :api-key)))))) + (let [config (dissoc (-> event :new :datastore-configuration) :state)] + (do-initialization-step new-config-state config)))))) diff --git a/src/app/cljs/one/repmax/datastore_configuration/view.cljs b/src/app/cljs/one/repmax/datastore_configuration/view.cljs index 2923637..6b185f5 100644 --- a/src/app/cljs/one/repmax/datastore_configuration/view.cljs +++ b/src/app/cljs/one/repmax/datastore_configuration/view.cljs @@ -44,11 +44,13 @@ (d/swap-content! header (:datastore-configuration-header snippets)) (d/swap-content! content (:datastore-configuration-form snippets)) (d/set-value! (d/by-id "api-key-input") (:api-key datastore-configuration)) + (d/set-value! (d/by-id "database-input") (:database datastore-configuration)) (event/listen (d/by-id "datastore-configuration-form-button") event-type/CLICK #(dispatch/fire :action - {:action :datastore-configuration/update - :api-key (d/value (d/by-id "api-key-input"))})))) + {:action :datastore-configuration/update + :api-key (d/value (d/by-id "api-key-input")) + :database (d/value (d/by-id "database-input"))})))) (defmulti render-datastore-configuration-state (fn [datastore-configuration] (:state datastore-configuration))) @@ -100,10 +102,12 @@ (defn disable-datastore-configuration-form [] (disable "api-key-input") + (disable "database-input") (disable "datastore-configuration-form-button")) (defn enable-datastore-configuration-form [] (enable "api-key-input") + (enable "database-input") (enable "datastore-configuration-form-button")) ;;; Register reactors diff --git a/src/app/cljs/one/repmax/exercises/observer.cljs b/src/app/cljs/one/repmax/exercises/observer.cljs index bb697d9..18b4c6c 100644 --- a/src/app/cljs/one/repmax/exercises/observer.cljs +++ b/src/app/cljs/one/repmax/exercises/observer.cljs @@ -10,7 +10,7 @@ ; TODO Add on-error callback (defmethod do-after :datastore-configuration/ready [{:keys [new]}] - (mongo/find-documents (-> new :datastore-configuration :api-key) + (mongo/find-documents (:datastore-configuration new) "exercises" (fn [data] (dispatch/fire :action {:action :exercises/initialized-from-datastore, :exercises data})) diff --git a/src/app/cljs/one/repmax/model.cljs b/src/app/cljs/one/repmax/model.cljs index 3dd5e83..71ecce3 100644 --- a/src/app/cljs/one/repmax/model.cljs +++ b/src/app/cljs/one/repmax/model.cljs @@ -6,7 +6,7 @@ [one.repmax.mongohq :as mongo])) (def initial-state {:state :start - :datastore-configuration {:state :obtain-credentials, :api-key ""} + :datastore-configuration {:state :obtain-credentials, :api-key "", :database ""} :exercises nil :exercise-search {:query nil, :exercise-ids nil}}) @@ -38,9 +38,11 @@ ; The :datastore-configuration map contains the following elements: ; ; 1. :api-key => the user-provided API key for use in accessing MongoHQ -; 2. :state => the overall state of the datastore configuration (i.e., +; 2. :database => the user-provided MongoHQ database name, indicating +; the database from which the app will read/write its data +; 3. :state => the overall state of the datastore configuration (i.e., ; the state of the datastore initialization/verification process) -; 3. :error => a map containing a description of the error that occured +; 4. :error => a map containing a description of the error that occured ; during the initialization process (in the :text key) and the state ; in which the error occured (in the :occured-in-state key); this map ; is only present if an error has occured @@ -62,16 +64,17 @@ (assoc :datastore-configuration (datastore-configuration-from-cookies)))) (defn datastore-configuration-from-cookies [] - (let [api-key (cookies/get-cookie :api-key)] - (if (nil? api-key) - (:datastore-configuration initial-state) - (datastore-configuration-for-new-api-key api-key)))) + (let [api-key (cookies/get-cookie :api-key) + database (cookies/get-cookie :database)] + (new-datastore-configuration api-key database))) -(defn datastore-configuration-for-new-api-key [api-key] - {:api-key api-key :state :verify-credentials}) +(defn new-datastore-configuration [api-key database] + (if (or (nil? api-key) (nil? database)) + (:datastore-configuration initial-state) + {:state :verify-credentials :api-key api-key :database database})) -(defmethod update-model :datastore-configuration/update [state {:keys [api-key]}] - (assoc state :datastore-configuration (datastore-configuration-for-new-api-key api-key))) +(defmethod update-model :datastore-configuration/update [state {:keys [api-key database]}] + (assoc state :datastore-configuration (new-datastore-configuration api-key database))) (defmethod update-model :datastore-configuration/credentials-verified [state _] (assoc-in state [:datastore-configuration :state] :verify-database)) diff --git a/src/app/cljs/one/repmax/mongohq.cljs b/src/app/cljs/one/repmax/mongohq.cljs index f133cde..4942633 100644 --- a/src/app/cljs/one/repmax/mongohq.cljs +++ b/src/app/cljs/one/repmax/mongohq.cljs @@ -1,23 +1,11 @@ (ns one.repmax.mongohq (:refer-clojure :exclude [sort]) + (:use [one.repmax.url-blueprint :only [->url]] + [one.repmax.util :only [clj->js]]) (:require [clojure.walk :as walk] [goog.json :as gjson] - [one.browser.remote :as remote])) - -; clj->js is from Chris Granger's excellent fetch library -; https://github.com/ibdknox/fetch/blob/30e938c/src/fetch/util.cljs -(defn- clj->js - "Recursively transforms ClojureScript maps into Javascript objects, - other ClojureScript colls into JavaScript arrays, and ClojureScript - keywords into JavaScript strings." - [x] - (cond - (string? x) x - (keyword? x) (name x) - (map? x) (.-strobj (reduce (fn [m [k v]] - (assoc m (clj->js k) (clj->js v))) {} x)) - (coll? x) (apply array (map clj->js x)) - :else x)) + [one.browser.remote :as remote] + [one.repmax.url-blueprint :as url-blueprint])) (defn- clj->json "Returns a JSON string representing the given ClojureScript data structure. @@ -30,23 +18,6 @@ (def ^{:private true} request-id (atom 0)) (defn- next-request-id [] (swap! request-id inc)) -(defn- map->query-string - "Returns a query string representation of the given map of query parameters. - - For example, the following example map of parameters: - - {:_apikey \"some-key\", :name \"some-name\"} - - Produces the following query string: - - \"_apikey=some-key&name=some-name\" - " - [params] - (.toString (.createFromMap goog.Uri.QueryData (clj->js params)))) - -(defn- with-params [url params] - (str url "?" (map->query-string params))) - (defn- request-headers [content-type] (if (= content-type :json) {"Content-Type" "application/json"})) @@ -73,50 +44,56 @@ :on-success #(on-success (response-body->clj %)) :on-error #(on-error ("error" (response-body->clj %))))) -(def ^{:private true} root-url "https://api.mongohq.com") +;;; URLs for API endpoints -;;; Working with Databases +(def ^{:private true} root-url "https://api.mongohq.com") +(def ^{:private true} root-databases-url (str root-url "/databases")) +(def ^{:private true} root-database-url (str root-databases-url "/:database")) +(def ^{:private true} root-collections-url (str root-database-url "/collections")) +(def ^{:private true} root-collection-url (str root-collections-url "/:collection")) +(def ^{:private true} root-documents-url (str root-collection-url "/documents")) -(def database-name "one-rep-max") +(defn- url-blueprint [base-url config] + {:base-url base-url + :segments {:database (:database config)} + :params {:_apikey (:api-key config)}}) -(def ^{:private true} root-databases-url (str root-url "/databases")) +(defn- documents-url-blueprint [config collection-name] + (-> (url-blueprint root-documents-url config) + (assoc-in [:segments :collection] collection-name))) -(def ^{:private true} root-database-url (str root-databases-url "/" database-name)) +;;; Working with Databases -(defn find-databases [api-key on-success on-error] - (request (with-params root-databases-url {:_apikey api-key}) - :on-success on-success - :on-error on-error)) +(defn find-databases [config on-success on-error] + (let [url (->url (url-blueprint root-databases-url config))] + (request url + :on-success on-success + :on-error on-error))) -(defn find-database [api-key on-success on-error] - (request (with-params root-database-url {:_apikey api-key}) - :on-success on-success - :on-error on-error)) +(defn find-database [config on-success on-error] + (let [url (->url (url-blueprint root-database-url config))] + (request url + :on-success on-success + :on-error on-error))) ;;; Working with Collections -(def ^{:private true} root-collections-url (str root-database-url "/collections")) - -(defn- root-collection-url [collection-name] - (str root-collections-url "/" collection-name)) - -(defn find-collections [api-key on-success on-error] - (request (with-params root-collections-url {:_apikey api-key}) - :on-success on-success - :on-error on-error)) +(defn find-collections [config on-success on-error] + (let [url (->url (url-blueprint root-collections-url config))] + (request url + :on-success on-success + :on-error on-error))) -(defn create-collection [api-key collection-name on-success on-error] - (request (with-params root-collections-url {:_apikey api-key}) - :method "POST" - :content (str "name=" collection-name) - :on-success on-success - :on-error on-error)) +(defn create-collection [config collection-name on-success on-error] + (let [url (->url (url-blueprint root-collections-url config))] + (request url + :method "POST" + :content (str "name=" collection-name) + :on-success on-success + :on-error on-error))) ;;; Working with Documents -(defn- root-documents-url [collection-name] - (str (root-collection-url collection-name) "/documents")) - (defn- simplify-object-ids "Takes a seq of maps, where each map is a Mongo document in raw format as returned by MongoHQ. @@ -162,35 +139,15 @@ number of documents, or all documents that match the given :query, whichever comes first. " - [api-key collection-name on-success & {:keys [limit query sort] - :or {limit js/Infinity}}] - (let [url (root-documents-url collection-name) - url-params {:_apikey api-key - :q (clj->json query) - :sort (clj->json sort)} - url-fn (with-partial-params url url-params)] - (find-documents-request url-fn + [config collection-name on-success & {:keys [limit query sort] + :or {limit js/Infinity}}] + (let [url-blueprint (-> (documents-url-blueprint config collection-name) + (assoc-in [:params :q] (clj->json query)) + (assoc-in [:params :sort] (clj->json sort)))] + (find-documents-request url-blueprint :limit-overall limit :on-success on-success))) -(defn- with-partial-params - "Takes a base URL string and a Clojure map of request parameters. - - Returns a function that takes a Clojure map of additional request parameters - and returns a URL (as a string) containing the combined set of request - parameters. - - For example: - - ((with-partial-params \"http://google.com\" {:q \"clojure\"}) {:hl :en}) - - Produces the following URL string: - - \"http://google.com?q=clojure&hl=en\" - " - [url params] - #(with-params url (conj params %))) - (def ^{:doc "The maximum number of documents that MongoHQ will return for a single request (i.e., the maximum allowed value for the 'limit' parameter)."} @@ -200,8 +157,7 @@ (defn- find-documents-request "Issues a request to the given URL (provided via the url-fn argument) to find documents. - * url-fn A function that takes a map of query parameters and returns the target - URL with the given query parameters. (See also: with-partial-params.) + * url-blueprint A map containing a 'URL blueprint'. (See one.repmax.url-blueprint.) Optional keyword argments: * :skip Used for pagination in MongoHQ. Indicates the number of documents to @@ -220,19 +176,23 @@ request. The function must accept a single argument: a sequence of documents. " - [url-fn & {:keys [skip limit-per-request limit-overall previous-docs on-success] - :or {skip 0 - limit-per-request max-docs-per-request - limit-overall js/Infinity - previous-docs [] - on-success (fn [data])}}] - (request (url-fn {:skip skip, :limit limit-per-request}) - :on-success (find-documents-callback url-fn - skip - limit-per-request - limit-overall - previous-docs - on-success))) + [url-blueprint & {:keys [skip limit-per-request limit-overall previous-docs on-success] + :or {skip 0 + limit-per-request max-docs-per-request + limit-overall js/Infinity + previous-docs [] + on-success (fn [data])}}] + (let [url (-> url-blueprint + (assoc-in [:params :skip] skip) + (assoc-in [:params :limit] limit-per-request) + ->url)] + (request url + :on-success (find-documents-callback url-blueprint + skip + limit-per-request + limit-overall + previous-docs + on-success)))) (defn- find-documents-callback "Returns a callback function to be invoked after a successful request to @@ -258,7 +218,7 @@ continue this pattern until we have fetched the requested number of documents (limit-overall) or all documents in the collection (whichever is smaller). " - [url-fn skip limit-per-request limit-overall previous-docs on-success] + [url-blueprint skip limit-per-request limit-overall previous-docs on-success] (fn [new-docs] (let [docs (concat previous-docs new-docs) fetched-all-docs? (or @@ -266,18 +226,19 @@ (< (count new-docs) limit-per-request))] (if fetched-all-docs? (on-success (take limit-overall (documents->maps docs))) - (find-documents-request url-fn + (find-documents-request url-blueprint :skip (+ skip limit-per-request) :limit-per-request limit-per-request :limit-overall limit-overall :previous-docs docs :on-success on-success))))) -(defn create-document [api-key collection-name document on-success on-error] - (request (with-params (root-documents-url collection-name) {:_apikey api-key}) - :method "POST" - :content-type :json - :content {"document" document, "safe" true} - :on-success #(on-success (walk/keywordize-keys %)) - :on-error on-error)) +(defn create-document [config collection-name document on-success on-error] + (let [url (->url (documents-url-blueprint config collection-name))] + (request url + :method "POST" + :content-type :json + :content {"document" document, "safe" true} + :on-success #(on-success (walk/keywordize-keys %)) + :on-error on-error))) diff --git a/src/app/cljs/one/repmax/sets/observer.cljs b/src/app/cljs/one/repmax/sets/observer.cljs index 88b381d..06aa77b 100644 --- a/src/app/cljs/one/repmax/sets/observer.cljs +++ b/src/app/cljs/one/repmax/sets/observer.cljs @@ -11,7 +11,7 @@ (defmethod do-after :new-set/new [{:keys [new]}] (let [exercise-id (-> new :new-set :exercise :_id)] - (mongo/find-documents (-> new :datastore-configuration :api-key) + (mongo/find-documents (:datastore-configuration new) "sets" #(find-recent-sets-on-success-callback exercise-id %) :limit 50 @@ -28,7 +28,7 @@ reps (js/parseInt (-> new :new-set :reps)) exercise-id (-> new :new-set :exercise :_id) document {:exercise-id exercise-id, :weight weight, :reps reps}] - (mongo/create-document (-> new :datastore-configuration :api-key) + (mongo/create-document (:datastore-configuration new) "sets" document #(create-set-success-callback document %) diff --git a/src/app/cljs/one/repmax/url_blueprint.cljs b/src/app/cljs/one/repmax/url_blueprint.cljs new file mode 100644 index 0000000..3f56b79 --- /dev/null +++ b/src/app/cljs/one/repmax/url_blueprint.cljs @@ -0,0 +1,54 @@ +(ns one.repmax.url-blueprint + (:use [one.repmax.util :only [clj->js]])) + +(defn- map->query-string + "Returns a query string representation of the given map of query parameters. + + For example, the following example map of parameters: + + {:_apikey \"some-key\", :name \"some-name\"} + + Produces the following query string: + + \"_apikey=some-key&name=some-name\" + " + [params] + (.toString (.createFromMap goog.Uri.QueryData (clj->js params)))) + +(defn- apply-segments [base-url segments] + (reduce (fn [url-string [segment-name segment-value]] + (let [pattern (re-pattern (str ":" (name segment-name)))] + (clojure.string/replace url-string pattern segment-value))) + base-url + segments)) + +(defn ->url + "Returns a URL (as a string) for the given 'URL blueprint' map. + + The format of a URL blueprint map is as follows: + + * :base-url (required) - a string containing the scheme, host, port, and + path, with optional placeholder 'segments' + * :params (optional) - a map of parameters that will appear in the query + string of the URL + * :segments (optional) - a map of segment names and values that will be + plugged into the base-url + + Examples: + + (->url {:base-url \"http://example.com\"}) + + ;;=> \"http://example.com\" + + (->url {:base-url \"http://example.com/databases/:db/collections/:collection\" + :params {\"_api-key\" \"secret\"} + :segments {:db \"one-rep-max\", :collection \"sets\"}}) + + ;; => \"http://example.com/databases/one-rep-max/collections/sets?_api-key=secret\" + " + [blueprint] + (let [base (apply-segments (:base-url blueprint) (:segments blueprint)) + params (get blueprint :params {})] + (if (empty? params) + base + (str base "?" (map->query-string params))))) diff --git a/src/app/cljs/one/repmax/util.cljs b/src/app/cljs/one/repmax/util.cljs new file mode 100644 index 0000000..ad1a525 --- /dev/null +++ b/src/app/cljs/one/repmax/util.cljs @@ -0,0 +1,16 @@ +(ns one.repmax.util) + +; clj->js is from Chris Granger's excellent fetch library +; https://github.com/ibdknox/fetch/blob/30e938c/src/fetch/util.cljs +(defn- clj->js + "Recursively transforms ClojureScript maps into Javascript objects, + other ClojureScript colls into JavaScript arrays, and ClojureScript + keywords into JavaScript strings." + [x] + (cond + (string? x) x + (keyword? x) (name x) + (map? x) (.-strobj (reduce (fn [m [k v]] + (assoc m (clj->js k) (clj->js v))) {} x)) + (coll? x) (apply array (map clj->js x)) + :else x)) diff --git a/templates/datastore_configuration.html b/templates/datastore_configuration.html index f5338bb..09b5877 100644 --- a/templates/datastore_configuration.html +++ b/templates/datastore_configuration.html @@ -14,7 +14,8 @@

One Rep Max

... Beef ribs salami pork belly, cow ground round flank venison ham sirloin jowl. Bacon shoulder short loin, chuck fatback corned beef pork belly pig. Ribeye swine strip steak jowl pork chop. Bacon tongue pork chop biltong.

- + +