graphql used for invoices

This commit is contained in:
Bryce Covert
2018-04-12 10:17:15 -07:00
parent 4165c7d180
commit 7425f7f393
15 changed files with 266 additions and 115 deletions

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ data/
\#*\# \#*\#
.\#* .\#*
\.terraform \.terraform
.idea
*.iml

View File

@@ -27,6 +27,8 @@ services:
VIRTUAL_HOST: local.app.integreatconsult.com VIRTUAL_HOST: local.app.integreatconsult.com
database: database:
image: postgres:9-alpine image: postgres:9-alpine
ports:
- "5432:5432"
environment: environment:
POSTGRES_USER: ap POSTGRES_USER: ap
POSTGRES_PASSWORD: fifteen-invoices-imported! POSTGRES_PASSWORD: fifteen-invoices-imported!

View File

@@ -13,8 +13,10 @@
[org.clojure/java.jdbc "0.7.3"] [org.clojure/java.jdbc "0.7.3"]
[cljsjs/dropzone "4.3.0-0"] [cljsjs/dropzone "4.3.0-0"]
[clj-fuzzy "0.4.1"] [clj-fuzzy "0.4.1"]
[honeysql "0.9.2"]
[com.walmartlabs/lacinia "0.25.0"] [com.walmartlabs/lacinia "0.25.0"]
;; https://mvnrepository.com/artifact/postgresql/postgresql ;; https://mvnrepository.com/artifact/postgresql/postgresql
[vincit/venia "0.2.5"]
[postgresql/postgresql "9.3-1102.jdbc41"] [postgresql/postgresql "9.3-1102.jdbc41"]
[cljs-http "0.1.44"] [cljs-http "0.1.44"]
[clj-http "3.7.0"] [clj-http "3.7.0"]

View File

@@ -1,20 +1,33 @@
(ns auto-ap.db.companies (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] [auto-ap.entities.companies :as entity]
[clojure.edn :as edn] [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] (def base-query (sql/build :select :*
(db->clj x)) :from :companies))
(defn get-all [] (defn get-all []
(->> (j/query (get-conn) "SELECT * FROM companies") (query base-query))
(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)]))))
(defn get-by-id [id] (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))))

View File

@@ -1,66 +1,56 @@
(ns auto-ap.db.invoices (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.parse :as parse]
[auto-ap.db.companies :as companies] [auto-ap.db.companies :as companies]
[auto-ap.db.vendors :as vendors] [auto-ap.db.vendors :as vendors]
[auto-ap.entities.companies :as company] [auto-ap.entities.companies :as company]
[auto-ap.entities.vendors :as vendor] [auto-ap.entities.vendors :as vendor]
[clojure.java.jdbc :as j] [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] (defn insert-multi! [rows]
(j/insert-multi! (get-conn) (j/insert-multi! (get-conn)
:invoices :invoices
(map clj->db rows))) (map clj->db rows)))
(defn with-relations [results] (def base-query (sql/build :select :*
(let [companies (reduce :from :invoices))
#(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 %)))))))
(defn get-all [] (defn get-all []
(->> (j/query (get-conn) (query base-query))
(str " SELECT invoices.* "
" FROM invoices "))
(map db->clj)
with-relations
))
(defn approve [] (defn approve []
(map db->clj (j/update! (get-conn) :invoices {:imported true} [] ))) (j/update! (get-conn) :invoices {:imported true} [] ))
(defn reject [] (defn reject []
(j/delete! (get-conn) :invoices ["imported = false"])) (j/delete! (get-conn) :invoices ["imported = false"]))
(defn get-unpaid [company] (defn get-unpaid [company]
(if company (query
(with-relations (map db->clj (j/query (get-conn) ["SELECT * FROM invoices WHERE imported=true AND company_id = ?" (Integer/parseInt company)]))) (if company
(with-relations (map db->clj (j/query (get-conn) "SELECT * FROM invoices WHERE imported=true"))))) (-> base-query
(helpers/merge-where [:= :imported true])
(helpers/merge-where [:= :company-id company]))
(-> base-query
(helpers/merge-where [:= :imported true])))))
(defn get-pending [company] (defn get-pending [company]
(if company (query
(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)]))) (if company
(with-relations (map db->clj (j/query (get-conn) "SELECT * FROM invoices WHERE imported=false or imported is null"))))) (-> base-query
(helpers/merge-where [:= :imported false])
(helpers/merge-where [:= :company-id company]))
(-> base-query
(helpers/merge-where [:= :imported false])))))
(defn query [params] (defn get-graphql [{:keys [imported company-id]}]
(let [ks (keys params) (query
sql (str " SELECT * FROM invoices " (cond-> base-query
(when (seq params) (not (nil? imported)) (helpers/merge-where [:= :imported imported])
" WHERE ") (not (nil? company-id)) (helpers/merge-where [:= :company-id company-id]))))
(str/join " AND " (map (fn [k] (str (name k) " = ?")) ks)))
vs (map params ks)]
(j/query (get-conn) (into [sql] vs))))
(defn import [parsed-invoices companies vendors] (defn import [parsed-invoices companies vendors]
(insert-multi! (insert-multi!
@@ -71,4 +61,4 @@
:vendor-id (:id (first (filter #(= (:code %) vendor-code) vendors))) :vendor-id (:id (first (filter #(= (:code %) vendor-code) vendors)))
:imported false :imported false
:potential-duplicate false) :potential-duplicate false)
:vendor-code))))) :vendor-code)))))

View File

@@ -1,7 +1,9 @@
(ns auto-ap.db.utils (ns auto-ap.db.utils
(:require [clojure.string :as str] (:require [clojure.string :as str]
[clojure.edn :as edn] [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] (defn snake->kebab [s]
(str/replace s #"_" "-")) (str/replace s #"_" "-"))
@@ -52,3 +54,13 @@
:user "ap" :user "ap"
:password "fifteen-invoices-imported!"})) :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)))

View File

@@ -1,33 +1,46 @@
(ns auto-ap.db.vendors (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] [auto-ap.entities.vendors :as entities]
[clojure.edn :as edn] [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] (defn unparse [x]
(-> x (-> x
(select-keys entities/all-keys) (select-keys entities/all-keys)))
clj->db))
(def base-query (sql/build :select :*
:from :vendors))
(defn get-all [] (defn get-all []
(->> (j/query (get-conn) "SELECT * FROM vendors") (query base-query))
(map parse)))
(defn get-by-id [id] (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] (defn upsert [id data]
(j/update! (get-conn) :vendors (unparse data) ["id = ?" (Integer/parseInt id)] ) (-> (sql/build
(parse (first (j/query (get-conn) ["SELECT * FROM vendors WHERE id = ?" (Integer/parseInt id)])))) :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] (defn insert [data]
(parse (first (j/insert! (get-conn) (let [[id] (-> (sql/build :insert-into :vendors
:vendors :values [(unparse data)])
(unparse data))))) execute!)]
(get-by-id id)))
(defn find-with-reminders [] (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"]))))

View File

@@ -1,15 +1,16 @@
(ns auto-ap.graphql (ns auto-ap.graphql
(:require (:require
[com.walmartlabs.lacinia.util :refer [attach-resolvers]] [com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[com.walmartlabs.lacinia.schema :as schema] [com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia :refer [execute]] [com.walmartlabs.lacinia :refer [execute]]
[com.walmartlabs.lacinia.executor :as executor] [com.walmartlabs.lacinia.executor :as executor]
[com.walmartlabs.lacinia.resolve :as resolve] [com.walmartlabs.lacinia.resolve :as resolve]
[auto-ap.db.invoices :as invoices] [auto-ap.db.invoices :as invoices]
[auto-ap.db.vendors :as vendors] [auto-ap.db.vendors :as vendors]
[auto-ap.db.companies :as companies] [auto-ap.db.companies :as companies]
[auto-ap.db.utils :as utils] [auto-ap.db.utils :as utils]
[clojure.walk :as walk]) [clojure.walk :as walk]
[clojure.string :as str])
(:import (:import
(clojure.lang IPersistentMap))) (clojure.lang IPersistentMap)))
@@ -19,14 +20,20 @@
{ {
:company :company
{:fields {:id {:type 'Int} {:fields {:id {:type 'Int}
:name {:type 'String}}} :name {:type 'String}
:email {:type 'String}}}
:vendor :vendor
{:fields {:id {:type 'Int} {:fields {:id {:type 'Int}
:name {:type 'String}}} :name {:type 'String}
:invoice_reminder_schedule {:type 'String}}}
:invoice :invoice
{:fields {:id {:type 'Int} {:fields {:id {:type 'Int}
:total {:type 'String}
:invoice_number {:type 'String}
:date {:type 'String}
:company_id {:type 'Int} :company_id {:type 'Int}
:vendor {:type :vendor :vendor {:type :vendor
:resolve :get-vendor} :resolve :get-vendor}
:company {:type :company :company {:type :company
:resolve :get-company}}}} :resolve :get-company}}}}
@@ -44,22 +51,65 @@
{} {}
x)) 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] (defn get-invoice [context args value]
(println (<-graphql args))
(let [extra-context (let [extra-context
(cond-> {} (cond-> {}
(executor/selects-field? context :invoice/vendor) (assoc :vendor-cache (by (vendors/get-all) :id )) (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 )))] (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] (defn get-vendor [context args value]
(if-let [vendor-cache (:vendor-cache context)] (->graphql
(vendor-cache (:vendor_id value)) (if-let [vendor-cache (:vendor-cache context)]
(vendors/get-by-id (:vendor_id value)))) (vendor-cache (:vendor_id value))
(vendors/get-by-id (:vendor_id value)))))
(defn get-company [context args value] (defn get-company [context args value]
(if-let [company-cache (:company-cache context)] (->graphql
(company-cache (:company_id value)) (if-let [company-cache (:company-cache context)]
(companies/get-by-id (:company_id value)))) (company-cache (:company_id value))
(companies/get-by-id (:company_id value)))))
(def schema (def schema
(-> integreat-schema (-> integreat-schema
@@ -68,6 +118,8 @@
:get-company get-company}) :get-company get-company})
schema/compile)) schema/compile))
(defn simplify (defn simplify
"Converts all ordered maps nested within the map into standard hash maps, and "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." sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
@@ -81,9 +133,15 @@
(seq? node) (seq? node)
(vec node) (vec node)
(keyword? node)
(kebab node)
:else :else
node)) node))
m)) m))
(defn query [q] (defn query
(simplify (execute schema q nil nil))) ([q]
(simplify (execute schema q nil nil)))
([q v]
(simplify (execute schema q v nil))))

View File

@@ -3,6 +3,7 @@
[auto-ap.routes.utils :refer [wrap-secure wrap-spec]] [auto-ap.routes.utils :refer [wrap-secure wrap-spec]]
[auto-ap.entities.companies :as entity] [auto-ap.entities.companies :as entity]
[auto-ap.graphql :as ql] [auto-ap.graphql :as ql]
[clojure.edn :as edn]
[compojure.core :refer [GET PUT context defroutes [compojure.core :refer [GET PUT context defroutes
wrap-routes]])) wrap-routes]]))
@@ -11,7 +12,10 @@
(wrap-routes (wrap-routes
(context "/graphql" [] (context "/graphql" []
(GET "/" {:keys [query-params]} (GET "/" {:keys [query-params]}
{:status 200 (let [variables (some-> (query-params "variables")
:body (pr-str (ql/query (query-params "query"))) edn/read-string)]
:headers {"Content-Type" "application/edn"}})) (println variables)
{:status 200
:body (pr-str (ql/query (query-params "query") variables))
:headers {"Content-Type" "application/edn"}})))
wrap-secure)) wrap-secure))

View File

@@ -2,6 +2,7 @@
(:require [auto-ap.db.companies :as companies] (:require [auto-ap.db.companies :as companies]
[auto-ap.db.vendors :as vendors] [auto-ap.db.vendors :as vendors]
[auto-ap.db.invoices :as invoices] [auto-ap.db.invoices :as invoices]
[auto-ap.db.utils :refer [query]]
[auto-ap.parse :as parse] [auto-ap.parse :as parse]
[auto-ap.routes.utils :refer [wrap-secure]] [auto-ap.routes.utils :refer [wrap-secure]]
[compojure.core :refer [GET POST context defroutes [compojure.core :refer [GET POST context defroutes

View File

@@ -9,12 +9,14 @@
#(not (str/blank? %)))) #(not (str/blank? %))))
(s/def ::name ::required-identifier) (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 %) (s/def ::email (s/nilable (s/and string? (s/or :is-email #(re-matches email-regex %)
:is-empty #(= % ""))))) :is-empty #(= % "")))))
(s/def ::company (s/keys :req-un [::name] (s/def ::company (s/keys :req-un [::name]
:opt-un [::email :opt-un [::email
::data
::id])) ::id]))

View File

@@ -5,6 +5,9 @@
[cljs-time.coerce :as c] [cljs-time.coerce :as c]
[cljs-time.core :as time] [cljs-time.core :as time]
[cljs.core.async :refer [<!]] [cljs.core.async :refer [<!]]
[clojure.string :as str]
[clojure.walk :as walk]
[venia.core :as v]
[auto-ap.history :as p] [auto-ap.history :as p]
[pushy.core :as pushy])) [pushy.core :as pushy]))
@@ -54,3 +57,50 @@
(dates->date-times) (dates->date-times)
(conj on-success) (conj on-success)
(re-frame/dispatch))))))) (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 (<! (http/request {:method :get
:headers headers
:url (str "/api/graphql?query=" (js/encodeURIComponent query)
"&variables=" (pr-str (or variables {})))}))]
(if (>= (: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)))))))

View File

@@ -4,6 +4,7 @@
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.routes :as routes] [auto-ap.routes :as routes]
[auto-ap.effects :as effects] [auto-ap.effects :as effects]
[venia.core :as v]
[bidi.bidi :as bidi])) [bidi.bidi :as bidi]))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@@ -62,7 +63,7 @@
(re-frame/reg-event-db (re-frame/reg-event-db
::imported-invoices ::imported-invoices
(fn [db [_ new-invoices]] (fn [db [_ new-invoices]]
(assoc-in db [:invoices] new-invoices))) (assoc-in db [:invoices :pending] new-invoices)))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::approve-invoices ::approve-invoices
@@ -79,23 +80,22 @@
::view-pending-invoices ::view-pending-invoices
(fn [cofx []] (fn [cofx []]
{:db (assoc-in (:db cofx) [:status :loading] true) {:db (assoc-in (:db cofx) [:status :loading] true)
:http {:method :get :graphql {:token (-> cofx :db :user)
:token (-> cofx :db :user) :query-obj {:venia/queries [[:invoice
:uri (str "/api/invoices/pending" {:imported false :company_id (:id @(re-frame/subscribe [::subs/company]))}
(when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] [:id :total :invoice-number :date [:vendor [:name :id]] [:company [:name :id]]]]]}
(str "?company=" company-id)))
:on-success [::received-invoices :pending]}})) :on-success [::received-invoices :pending]}}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::view-unpaid-invoices ::view-unpaid-invoices
(fn [cofx []] (fn [cofx []]
{:db (assoc-in (:db cofx) [:status :loading] true) {:db (assoc-in (:db cofx) [:status :loading] true)
:http {:method :get :graphql {:token (-> cofx :db :user)
:token (-> cofx :db :user) :query-obj {:venia/queries [[:invoice
:uri (str "/api/invoices/unpaid" {:imported true :company_id (:id @(re-frame/subscribe [::subs/company]))}
(when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] [:id :total :invoice-number :date [:vendor [:name :id]] [:company [:name :id]]]]]}
(str "?company=" company-id))) :on-success [::received-invoices :unpaid]}}))
:on-success [::received-invoices :unpaid]}}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::reject-invoices ::reject-invoices
@@ -133,11 +133,13 @@
(re-frame/reg-event-db (re-frame/reg-event-db
::received-invoices ::received-invoices
(fn [db [_ type new-invoices]] (fn [db [_ type result]]
(-> db (let [new-invoices (if (:invoice result)
(assoc-in [:invoices type] new-invoices) (:invoice result)
(assoc-in [:status :loading] false) result)]
))) (-> db
(assoc-in [:invoices type] new-invoices)
(assoc-in [:status :loading] false)))))
(re-frame/reg-event-db (re-frame/reg-event-db
::change-form-state ::change-form-state

View File

@@ -58,11 +58,11 @@
[:th "Date"] [:th "Date"]
[:th "Amount"] [:th "Amount"]
[:th]]] [:th]]]
[:tbody (for [{:keys [vendor vendor-id potential-duplicate company-id customer-identifier invoice-number date total id] :as i} @invoices] [:tbody (for [{:keys [vendor vendor-id potential-duplicate company customer-identifier invoice-number date total id] :as i} @invoices]
^{:key (str company-id "-" invoice-number "-" date "-" total "-" id)} ^{:key id}
[:tr [:tr
[:td (:name (:vendor i))] [:td (:name (:vendor i))]
(if company-id (if company
[:td (:name (:company i))] [:td (:name (:company i))]
[:td [:i.icon.fa.fa-warning {:title "potential duplicate"}] [:td [:i.icon.fa.fa-warning {:title "potential duplicate"}]
(str "'" customer-identifier "' doesn't match any known company")]) (str "'" customer-identifier "' doesn't match any known company")])

View File

@@ -23,8 +23,8 @@
[:th "Invoice #"] [:th "Invoice #"]
[:th "Date"] [:th "Date"]
[:th "Amount"]]] [:th "Amount"]]]
[:tbody (for [{:keys [company vendor invoice-number date total id] :as i} @invoices] [:tbody (for [{:keys [company invoice-number date total id vendor] :as i} @invoices]
^{:key (str (:id company) "-" invoice-number "-" date "-" total "-" id)} ^{:key (str company "-" invoice-number "-" date "-" total "-" id)}
[:tr [:tr
[:td (:name vendor)] [:td (:name vendor)]
[:td (:name company)] [:td (:name company)]