diff --git a/.gitignore b/.gitignore index c176da72..134ee1a1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ data/ \#*\# .\#* \.terraform +.idea +*.iml diff --git a/docker-compose.yml b/docker-compose.yml index 85ebcecf..86b63cb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,8 @@ services: VIRTUAL_HOST: local.app.integreatconsult.com database: image: postgres:9-alpine + ports: + - "5432:5432" environment: POSTGRES_USER: ap POSTGRES_PASSWORD: fifteen-invoices-imported! diff --git a/project.clj b/project.clj index 07b5ceba..93025f0e 100644 --- a/project.clj +++ b/project.clj @@ -13,8 +13,10 @@ [org.clojure/java.jdbc "0.7.3"] [cljsjs/dropzone "4.3.0-0"] [clj-fuzzy "0.4.1"] + [honeysql "0.9.2"] [com.walmartlabs/lacinia "0.25.0"] ;; https://mvnrepository.com/artifact/postgresql/postgresql + [vincit/venia "0.2.5"] [postgresql/postgresql "9.3-1102.jdbc41"] [cljs-http "0.1.44"] [clj-http "3.7.0"] diff --git a/src/clj/auto_ap/db/companies.clj b/src/clj/auto_ap/db/companies.clj index c5a1c9c9..e7f3d35b 100644 --- a/src/clj/auto_ap/db/companies.clj +++ b/src/clj/auto_ap/db/companies.clj @@ -1,20 +1,33 @@ (ns auto-ap.db.companies - (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn]] + (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn query execute!]] [auto-ap.entities.companies :as entity] [clojure.edn :as edn] - [clojure.java.jdbc :as j])) + [clojure.java.jdbc :as j] + [honeysql.core :as sql] + [honeysql.helpers :as helpers])) -(defn parse [x] - (db->clj x)) +(def base-query (sql/build :select :* + :from :companies)) (defn get-all [] - (->> (j/query (get-conn) "SELECT * FROM companies") - (map parse))) - -(defn upsert [id data] - (j/update! (get-conn) :companies (clj->db data) ["id = ?" (Integer/parseInt id)] ) - (parse (first (j/query (get-conn) ["SELECT * FROM companies WHERE id = ?" (Integer/parseInt id)])))) + (query base-query)) (defn get-by-id [id] - (parse (first (j/query (get-conn) ["SELECT * FROM companies WHERE id = ?" id])))) + (first (query (-> base-query + (helpers/merge-where [:= :id id]))))) +(defn upsert [id data] + (prn (clj->db (select-keys data entity/all-keys))) + (-> (sql/build + :update :companies + :set (clj->db (select-keys data entity/all-keys )) + :where [:= :id (if (int? id) + id + (Integer/parseInt id))]) + execute!) + (get-by-id (if (int? id) + id + (Integer/parseInt id)))) + + + diff --git a/src/clj/auto_ap/db/invoices.clj b/src/clj/auto_ap/db/invoices.clj index b578c1cb..66b70009 100644 --- a/src/clj/auto_ap/db/invoices.clj +++ b/src/clj/auto_ap/db/invoices.clj @@ -1,66 +1,56 @@ (ns auto-ap.db.invoices - (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn]] + (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn query] :as utils] [auto-ap.parse :as parse] [auto-ap.db.companies :as companies] [auto-ap.db.vendors :as vendors] [auto-ap.entities.companies :as company] [auto-ap.entities.vendors :as vendor] [clojure.java.jdbc :as j] - [clojure.string :as str])) + [clojure.string :as str] + [honeysql.core :as sql] + [honeysql.helpers :as helpers])) (defn insert-multi! [rows] (j/insert-multi! (get-conn) :invoices (map clj->db rows))) -(defn with-relations [results] - (let [companies (reduce - #(assoc %1 (:id %2) %2) - {} - (companies/get-all)) - vendors (reduce - #(assoc %1 (:id %2) %2) - {} - (vendors/get-all))] - (println companies vendors) - (->> results - (map #(assoc % :vendor (vendors (:vendor-id %)))) - (map #(assoc % :company (companies (:company-id %))))))) +(def base-query (sql/build :select :* + :from :invoices)) + (defn get-all [] - (->> (j/query (get-conn) - (str " SELECT invoices.* " - " FROM invoices ")) - (map db->clj) - with-relations - )) - + (query base-query)) (defn approve [] - (map db->clj (j/update! (get-conn) :invoices {:imported true} [] ))) + (j/update! (get-conn) :invoices {:imported true} [] )) (defn reject [] (j/delete! (get-conn) :invoices ["imported = false"])) (defn get-unpaid [company] - (if company - (with-relations (map db->clj (j/query (get-conn) ["SELECT * FROM invoices WHERE imported=true AND company_id = ?" (Integer/parseInt company)]))) - (with-relations (map db->clj (j/query (get-conn) "SELECT * FROM invoices WHERE imported=true"))))) + (query + (if company + (-> base-query + (helpers/merge-where [:= :imported true]) + (helpers/merge-where [:= :company-id company])) + (-> base-query + (helpers/merge-where [:= :imported true]))))) (defn get-pending [company] - (if company - (with-relations (map db->clj (j/query (get-conn) ["SELECT * FROM invoices WHERE (imported=false or imported is null) AND company_id = ?" (Integer/parseInt company)]))) - (with-relations (map db->clj (j/query (get-conn) "SELECT * FROM invoices WHERE imported=false or imported is null"))))) + (query + (if company + (-> base-query + (helpers/merge-where [:= :imported false]) + (helpers/merge-where [:= :company-id company])) + (-> base-query + (helpers/merge-where [:= :imported false]))))) -(defn query [params] - (let [ks (keys params) - sql (str " SELECT * FROM invoices " - (when (seq params) - " WHERE ") - (str/join " AND " (map (fn [k] (str (name k) " = ?")) ks))) - vs (map params ks)] - - (j/query (get-conn) (into [sql] vs)))) +(defn get-graphql [{:keys [imported company-id]}] + (query + (cond-> base-query + (not (nil? imported)) (helpers/merge-where [:= :imported imported]) + (not (nil? company-id)) (helpers/merge-where [:= :company-id company-id])))) (defn import [parsed-invoices companies vendors] (insert-multi! @@ -71,4 +61,4 @@ :vendor-id (:id (first (filter #(= (:code %) vendor-code) vendors))) :imported false :potential-duplicate false) - :vendor-code))))) + :vendor-code))))) diff --git a/src/clj/auto_ap/db/utils.clj b/src/clj/auto_ap/db/utils.clj index 68af4984..8eb5efa2 100644 --- a/src/clj/auto_ap/db/utils.clj +++ b/src/clj/auto_ap/db/utils.clj @@ -1,7 +1,9 @@ -(ns auto-ap.db.utils + (ns auto-ap.db.utils (:require [clojure.string :as str] [clojure.edn :as edn] - [config.core :refer [env]])) + [clojure.java.jdbc :as j] + [config.core :refer [env]] + [honeysql.core :as sql])) (defn snake->kebab [s] (str/replace s #"_" "-")) @@ -52,3 +54,13 @@ :user "ap" :password "fifteen-invoices-imported!"})) +(defn query [q] + (let [formatted (sql/format q)] + (println "Executing query " q " SQL: " formatted) + + (map db->clj (j/query (get-conn) formatted)))) + +(defn execute! [q] + (let [formatted (sql/format q)] + (println "Executing query " q " SQL: " formatted) + (j/execute! (get-conn) formatted))) diff --git a/src/clj/auto_ap/db/vendors.clj b/src/clj/auto_ap/db/vendors.clj index 449cad78..0824c8c1 100644 --- a/src/clj/auto_ap/db/vendors.clj +++ b/src/clj/auto_ap/db/vendors.clj @@ -1,33 +1,46 @@ (ns auto-ap.db.vendors - (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn]] + (:require [auto-ap.db.utils :refer [clj->db db->clj get-conn query execute!]] [auto-ap.entities.vendors :as entities] [clojure.edn :as edn] - [clojure.java.jdbc :as j])) + [clojure.java.jdbc :as j] + [honeysql.core :as sql] + [honeysql.helpers :as helpers] + [honeysql.format :as f])) -(defn parse [x] - (db->clj x)) - (defn unparse [x] (-> x - (select-keys entities/all-keys) - clj->db)) + (select-keys entities/all-keys))) + +(def base-query (sql/build :select :* + :from :vendors)) + (defn get-all [] - (->> (j/query (get-conn) "SELECT * FROM vendors") - (map parse))) + (query base-query)) (defn get-by-id [id] - (parse (first (j/query (get-conn) ["SELECT * FROM vendors WHERE id = ?" id])))) + (first (query (-> base-query + (helpers/merge-where [:= :id id]))))) (defn upsert [id data] - (j/update! (get-conn) :vendors (unparse data) ["id = ?" (Integer/parseInt id)] ) - (parse (first (j/query (get-conn) ["SELECT * FROM vendors WHERE id = ?" (Integer/parseInt id)])))) + (-> (sql/build + :update :vendors + :set (unparse data) + :where [:= :id (if (int? id) + id + (Integer/parseInt id))]) + execute!) + (get-by-id (if (int? id) + id + (Integer/parseInt id)))) (defn insert [data] - (parse (first (j/insert! (get-conn) - :vendors - (unparse data))))) + (let [[id] (-> (sql/build :insert-into :vendors + :values [(unparse data)]) + execute!)] + (get-by-id id))) (defn find-with-reminders [] - (map parse (j/query (get-conn) ["SELECT * FROM vendors WHERE invoice_reminder_schedule = ?" "Weekly"]))) + (query (-> base-query + (helpers/merge-where [:= :invoice-reminder-schedule "Weekly"])))) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 7c191875..9d42d615 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -1,15 +1,16 @@ (ns auto-ap.graphql (:require - [com.walmartlabs.lacinia.util :refer [attach-resolvers]] - [com.walmartlabs.lacinia.schema :as schema] - [com.walmartlabs.lacinia :refer [execute]] - [com.walmartlabs.lacinia.executor :as executor] - [com.walmartlabs.lacinia.resolve :as resolve] - [auto-ap.db.invoices :as invoices] - [auto-ap.db.vendors :as vendors] - [auto-ap.db.companies :as companies] - [auto-ap.db.utils :as utils] - [clojure.walk :as walk]) + [com.walmartlabs.lacinia.util :refer [attach-resolvers]] + [com.walmartlabs.lacinia.schema :as schema] + [com.walmartlabs.lacinia :refer [execute]] + [com.walmartlabs.lacinia.executor :as executor] + [com.walmartlabs.lacinia.resolve :as resolve] + [auto-ap.db.invoices :as invoices] + [auto-ap.db.vendors :as vendors] + [auto-ap.db.companies :as companies] + [auto-ap.db.utils :as utils] + [clojure.walk :as walk] + [clojure.string :as str]) (:import (clojure.lang IPersistentMap))) @@ -19,14 +20,20 @@ { :company {:fields {:id {:type 'Int} - :name {:type 'String}}} + :name {:type 'String} + :email {:type 'String}}} :vendor {:fields {:id {:type 'Int} - :name {:type 'String}}} + :name {:type 'String} + :invoice_reminder_schedule {:type 'String}}} :invoice {:fields {:id {:type 'Int} + :total {:type 'String} + :invoice_number {:type 'String} + :date {:type 'String} :company_id {:type 'Int} :vendor {:type :vendor + :resolve :get-vendor} :company {:type :company :resolve :get-company}}}} @@ -44,22 +51,65 @@ {} x)) +(defn snake->kebab [s] + (str/replace s #"_" "-")) + +(defn kebab [x] + (keyword (snake->kebab (name x)))) + +(defn kebab->snake [s] + (str/replace s #"-" "_")) + +(defn snake [x] + (keyword (kebab->snake (name x)))) + +(defn ->graphql [m] + (walk/postwalk + (fn [node] + (cond + + (keyword? node) + (snake node) + + :else + node)) + m)) + +(defn <-graphql [m] + (walk/postwalk + (fn [node] + (cond + + (keyword? node) + (kebab node) + + :else + node)) + m)) + (defn get-invoice [context args value] + + (println (<-graphql args)) (let [extra-context (cond-> {} (executor/selects-field? context :invoice/vendor) (assoc :vendor-cache (by (vendors/get-all) :id )) (executor/selects-field? context :invoice/company) (assoc :company-cache (by (companies/get-all) :id )))] - (resolve/with-context (invoices/query args) extra-context))) + (resolve/with-context + (map + ->graphql + (invoices/get-graphql (<-graphql args))) extra-context))) (defn get-vendor [context args value] - (if-let [vendor-cache (:vendor-cache context)] - (vendor-cache (:vendor_id value)) - (vendors/get-by-id (:vendor_id value)))) + (->graphql + (if-let [vendor-cache (:vendor-cache context)] + (vendor-cache (:vendor_id value)) + (vendors/get-by-id (:vendor_id value))))) (defn get-company [context args value] - (if-let [company-cache (:company-cache context)] - (company-cache (:company_id value)) - (companies/get-by-id (:company_id value)))) + (->graphql + (if-let [company-cache (:company-cache context)] + (company-cache (:company_id value)) + (companies/get-by-id (:company_id value))))) (def schema (-> integreat-schema @@ -68,6 +118,8 @@ :get-company get-company}) schema/compile)) + + (defn simplify "Converts all ordered maps nested within the map into standard hash maps, and sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems." @@ -81,9 +133,15 @@ (seq? node) (vec node) + (keyword? node) + (kebab node) + :else node)) m)) -(defn query [q] - (simplify (execute schema q nil nil))) +(defn query + ([q] + (simplify (execute schema q nil nil))) + ([q v] + (simplify (execute schema q v nil)))) diff --git a/src/clj/auto_ap/routes/graphql.clj b/src/clj/auto_ap/routes/graphql.clj index 6384a106..f8f9acd2 100644 --- a/src/clj/auto_ap/routes/graphql.clj +++ b/src/clj/auto_ap/routes/graphql.clj @@ -3,6 +3,7 @@ [auto-ap.routes.utils :refer [wrap-secure wrap-spec]] [auto-ap.entities.companies :as entity] [auto-ap.graphql :as ql] + [clojure.edn :as edn] [compojure.core :refer [GET PUT context defroutes wrap-routes]])) @@ -11,7 +12,10 @@ (wrap-routes (context "/graphql" [] (GET "/" {:keys [query-params]} - {:status 200 - :body (pr-str (ql/query (query-params "query"))) - :headers {"Content-Type" "application/edn"}})) + (let [variables (some-> (query-params "variables") + edn/read-string)] + (println variables) + {:status 200 + :body (pr-str (ql/query (query-params "query") variables)) + :headers {"Content-Type" "application/edn"}}))) wrap-secure)) diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index efcb53b0..b45e55ac 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -2,6 +2,7 @@ (:require [auto-ap.db.companies :as companies] [auto-ap.db.vendors :as vendors] [auto-ap.db.invoices :as invoices] + [auto-ap.db.utils :refer [query]] [auto-ap.parse :as parse] [auto-ap.routes.utils :refer [wrap-secure]] [compojure.core :refer [GET POST context defroutes diff --git a/src/cljc/auto_ap/entities/companies.cljc b/src/cljc/auto_ap/entities/companies.cljc index 32db6041..a565fa4f 100644 --- a/src/cljc/auto_ap/entities/companies.cljc +++ b/src/cljc/auto_ap/entities/companies.cljc @@ -9,12 +9,14 @@ #(not (str/blank? %)))) (s/def ::name ::required-identifier) +(s/def ::data map?) (s/def ::email (s/nilable (s/and string? (s/or :is-email #(re-matches email-regex %) :is-empty #(= % ""))))) (s/def ::company (s/keys :req-un [::name] :opt-un [::email + ::data ::id])) diff --git a/src/cljs/auto_ap/effects.cljs b/src/cljs/auto_ap/effects.cljs index e3b42cfc..99e3c4db 100644 --- a/src/cljs/auto_ap/effects.cljs +++ b/src/cljs/auto_ap/effects.cljs @@ -5,6 +5,9 @@ [cljs-time.coerce :as c] [cljs-time.core :as time] [cljs.core.async :refer [date-times) (conj on-success) (re-frame/dispatch))))))) + +(defn kebab->snake [s] + (str/replace s #"-" "_")) + +(defn snake [x] + (if (namespace x) + (keyword (namespace x) (kebab->snake (name x))) + (keyword (kebab->snake (name x))))) + +(defn ->graphql [m] + (walk/postwalk + (fn [node] + (cond + + (keyword? node) + (snake node) + + :else + node)) + m)) + +(re-frame/reg-fx + :graphql + (fn [{:keys [query on-success on-error token variables query-obj]}] + (go + (let [headers (if token + {"Authorization" (str "Token " token)} + {}) + query (or query (v/graphql-query (->graphql query-obj))) + response (= (:status response) 400) + (when on-error + (->> response + :body + :data + (dates->date-times) + (conj on-error) + (re-frame/dispatch))) + (->> response + :body + :data + (dates->date-times) + (conj on-success) + (re-frame/dispatch))))))) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index 24d5e587..92879150 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -4,6 +4,7 @@ [auto-ap.subs :as subs] [auto-ap.routes :as routes] [auto-ap.effects :as effects] + [venia.core :as v] [bidi.bidi :as bidi])) (re-frame/reg-event-fx @@ -62,7 +63,7 @@ (re-frame/reg-event-db ::imported-invoices (fn [db [_ new-invoices]] - (assoc-in db [:invoices] new-invoices))) + (assoc-in db [:invoices :pending] new-invoices))) (re-frame/reg-event-fx ::approve-invoices @@ -79,23 +80,22 @@ ::view-pending-invoices (fn [cofx []] {:db (assoc-in (:db cofx) [:status :loading] true) - :http {:method :get - :token (-> cofx :db :user) - :uri (str "/api/invoices/pending" - (when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] - (str "?company=" company-id))) - :on-success [::received-invoices :pending]}})) + :graphql {:token (-> cofx :db :user) + :query-obj {:venia/queries [[:invoice + {:imported false :company_id (:id @(re-frame/subscribe [::subs/company]))} + [:id :total :invoice-number :date [:vendor [:name :id]] [:company [:name :id]]]]]} + + :on-success [::received-invoices :pending]}})) (re-frame/reg-event-fx ::view-unpaid-invoices (fn [cofx []] {:db (assoc-in (:db cofx) [:status :loading] true) - :http {:method :get - :token (-> cofx :db :user) - :uri (str "/api/invoices/unpaid" - (when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] - (str "?company=" company-id))) - :on-success [::received-invoices :unpaid]}})) + :graphql {:token (-> cofx :db :user) + :query-obj {:venia/queries [[:invoice + {:imported true :company_id (:id @(re-frame/subscribe [::subs/company]))} + [:id :total :invoice-number :date [:vendor [:name :id]] [:company [:name :id]]]]]} + :on-success [::received-invoices :unpaid]}})) (re-frame/reg-event-fx ::reject-invoices @@ -133,11 +133,13 @@ (re-frame/reg-event-db ::received-invoices - (fn [db [_ type new-invoices]] - (-> db - (assoc-in [:invoices type] new-invoices) - (assoc-in [:status :loading] false) - ))) + (fn [db [_ type result]] + (let [new-invoices (if (:invoice result) + (:invoice result) + result)] + (-> db + (assoc-in [:invoices type] new-invoices) + (assoc-in [:status :loading] false))))) (re-frame/reg-event-db ::change-form-state diff --git a/src/cljs/auto_ap/views/pages/import_invoices.cljs b/src/cljs/auto_ap/views/pages/import_invoices.cljs index 830d75ac..a8f9e907 100644 --- a/src/cljs/auto_ap/views/pages/import_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/import_invoices.cljs @@ -58,11 +58,11 @@ [:th "Date"] [:th "Amount"] [:th]]] - [:tbody (for [{:keys [vendor vendor-id potential-duplicate company-id customer-identifier invoice-number date total id] :as i} @invoices] - ^{:key (str company-id "-" invoice-number "-" date "-" total "-" id)} + [:tbody (for [{:keys [vendor vendor-id potential-duplicate company customer-identifier invoice-number date total id] :as i} @invoices] + ^{:key id} [:tr [:td (:name (:vendor i))] - (if company-id + (if company [:td (:name (:company i))] [:td [:i.icon.fa.fa-warning {:title "potential duplicate"}] (str "'" customer-identifier "' doesn't match any known company")]) diff --git a/src/cljs/auto_ap/views/pages/paid_invoices.cljs b/src/cljs/auto_ap/views/pages/paid_invoices.cljs index 78808b1b..146199e1 100644 --- a/src/cljs/auto_ap/views/pages/paid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/paid_invoices.cljs @@ -23,8 +23,8 @@ [:th "Invoice #"] [:th "Date"] [:th "Amount"]]] - [:tbody (for [{:keys [company vendor invoice-number date total id] :as i} @invoices] - ^{:key (str (:id company) "-" invoice-number "-" date "-" total "-" id)} + [:tbody (for [{:keys [company invoice-number date total id vendor] :as i} @invoices] + ^{:key (str company "-" invoice-number "-" date "-" total "-" id)} [:tr [:td (:name vendor)] [:td (:name company)]