backend on postgres

This commit is contained in:
Daniel O'Connell 2021-02-22 23:01:26 +01:00
parent 3a340fd801
commit c1c695a978
13 changed files with 297 additions and 77 deletions

View File

@ -4,6 +4,8 @@
* daily view
* infinite scroll
* Move to different day
* handle regular customers
** every n days
** copy over to next week
@ -13,6 +15,10 @@
* products CRM
## Start
docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
A [re-frame](https://github.com/day8/re-frame) application designed to ... well, that part is up to
you.

View File

@ -12,6 +12,8 @@
[ns-tracker "0.4.0"]
[compojure "1.6.2"]
[yogthos/config "1.1.7"]
[seancorfield/next.jdbc "1.1.613"]
[org.postgresql/postgresql "42.2.6"]
[ring-basic-authentication "1.1.0"]
[ring-cors "0.1.13"]
[ring "1.8.1"]]

View File

@ -0,0 +1,53 @@
CREATE EXTENSION pgcrypto;
CREATE TABLE users (
id SERIAL,
name VARCHAR(256) UNIQUE,
password VARCHAR(256),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(id)
);
CREATE TABLE customers (
id SERIAL,
name VARCHAR(512) UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted BOOLEAN,
user_id INT,
PRIMARY KEY(id),
CONSTRAINT fk_users FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE products (
id SERIAL,
name VARCHAR(512) UNIQUE,
amount NUMERIC,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted BOOLEAN,
user_id INT,
PRIMARY KEY(id),
CONSTRAINT fk_users FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TYPE order_state AS ENUM('waiting', 'fulfilled', 'canceled');
CREATE TABLE orders (
id SERIAL,
customer_id INT,
notes TEXT,
status order_state DEFAULT 'waiting',
order_date TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id INT,
PRIMARY KEY(id),
CONSTRAINT fk_customer FOREIGN KEY(customer_id) REFERENCES customers(id),
CONSTRAINT fk_users FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE order_products (
id SERIAL,
order_id INT,
product_id INT,
amount NUMERIC,
PRIMARY KEY(id),
CONSTRAINT fk_order FOREIGN KEY(order_id) REFERENCES orders(id),
CONSTRAINT fk_product FOREIGN KEY(product_id) REFERENCES products(id)
);

View File

@ -0,0 +1,20 @@
(ns chicken-master.customers
(:require [next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
[chicken-master.db :as db]
[chicken-master.orders :as orders]))
(defn get-all []
(->> (sql/query db/db-uri ["select * from customers where deleted is null"])
(map (fn [{:customers/keys [id name]}] {:id id :name name}))))
(defn create! [name]
(jdbc/execute! db/db-uri
["INSERT INTO customers (name) VALUES(?) ON CONFLICT (name) DO UPDATE SET deleted = NULL"
name])
{:customers (get-all)})
(defn delete! [id]
(sql/update! db/db-uri :customers {:deleted true} {:id id})
{:orders (orders/get-all)
:customers (get-all)})

View File

@ -0,0 +1,26 @@
(ns chicken-master.db
(:require [clojure.string :as str]
[next.jdbc :as jdbc]
[next.jdbc.types :as jdbc.types]
[next.jdbc.sql :as sql]
[chicken-master.time :as t]))
(def db-uri {:jdbcUrl (or (System/getenv "DB_URI") "jdbc:postgresql://localhost/postgres?user=postgres&password=mysecretpassword")})
(defn psql-list
([items] (psql-list items ""))
([items prefix] (str "(" (str/join ", " (repeat (count items) (str prefix "?"))) ")")))
(defn create-user [name passwd]
(jdbc/execute-one!
db-uri
["INSERT INTO users (name, password) VALUES (?, crypt(?, gen_salt('bf')))" name passwd]))
(defn valid-user? [name passwd]
(jdbc/execute-one!
db-uri
[" SELECT * FROM users WHERE name = ? AND password = crypt(?, password)" name passwd]))
(comment
(create-user "siloa" "krach")
(valid-user? "siloa" "krach"))

View File

@ -1,5 +1,9 @@
(ns chicken-master.handler
(:require [chicken-master.mocks :as mocks]
[chicken-master.db :as db]
[chicken-master.orders :as orders]
[chicken-master.customers :as customers]
[chicken-master.products :as products]
[clojure.edn :as edn]
[compojure.core :refer [GET POST PUT DELETE defroutes]]
[compojure.route :refer [resources]]
@ -8,21 +12,21 @@
[ring.middleware.basic-authentication :refer [wrap-basic-authentication]]
[ring.middleware.cors :refer [wrap-cors]]))
(defn get-customers [] {:body (mocks/fetch-customers {})})
(defn add-customer [request] {:body (some-> request :body :name mocks/add-customer)})
(defn delete-customer [id] {:body (mocks/delete-customer (edn/read-string id))})
(defn get-customers [] {:body (customers/get-all)})
(defn add-customer [request] {:body (some-> request :body :name customers/create!)})
(defn delete-customer [id] {:body (customers/delete! (edn/read-string id))})
(defn get-products [_] {:body (mocks/get-all-products)})
(defn save-products [request] {:body (some-> request :body mocks/save-stocks)})
(defn get-products [_] {:body (products/get-all)})
(defn save-products [request] {:body (some-> request :body products/update!)})
(defn get-orders [params] {:body {:orders (mocks/get-orders params)}})
(defn get-orders [params] {:body {:orders (orders/get-all)}})
(defn update-order [request]
(let [id (some-> request :route-params :id (Integer/parseInt))
body (some->> request :body)]
{:body (mocks/replace-order id body)}))
order (-> request :body (update :id #(or % id)))]
{:body (orders/replace! order)}))
(defn delete-order [id] {:body (mocks/delete-order (edn/read-string id))})
(defn set-order-state [id status] {:body (mocks/order-state (edn/read-string id) status)})
(defn delete-order [id] {:body (orders/delete! (edn/read-string id))})
(defn set-order-state [id status] {:body (orders/change-state! (edn/read-string id) status)})
(defn get-stock [params]
{:body
@ -49,7 +53,7 @@
(defn- handle-edn [response]
(if (string? (:body response))
(if (-> response :body type #{java.io.File java.lang.String})
response
(-> response
(assoc-in [:headers "Content-Type"] "application/edn")
@ -71,8 +75,7 @@
request)))))
(defn authenticated? [name pass]
(and (= name "siloa")
(= pass "krach")))
(db/valid-user? name pass))
(def handler (-> routes
(wrap-basic-authentication authenticated?)

View File

@ -71,6 +71,8 @@
(defn get-orders [params] @orders)
(defn replace-order [id order]
(prn id)
(prn order)
(println "replacing order" order)
(let [prev-day (:day (@orders (:id order)))
order (update order :id #(or % (swap! id-counter inc)))]

View File

@ -0,0 +1,99 @@
(ns chicken-master.orders
(:require [next.jdbc :as jdbc]
[next.jdbc.types :as jdbc.types]
[next.jdbc.sql :as sql]
[chicken-master.db :as db]
[chicken-master.products :as products]
[chicken-master.time :as t]))
(defn- upsert-order! [tx user-id {:keys [id day state notes]}]
(let [order {:customer_id user-id
:notes notes
:status (some-> state name jdbc.types/as-other)
:order_date (some-> day t/parse-date t/inst->timestamp)}]
(if id
(do (sql/update! tx :orders order {:id id}) id)
(:orders/id (sql/insert! tx :orders order)))))
(defn- structure-order [items]
{:id (-> items first :orders/id)
:notes (-> items first :orders/notes)
:state (-> items first :orders/status keyword)
:day (-> items first :orders/order_date (.toInstant) str (subs 0 10))
:who {:id (-> items first :customers/id)
:name (-> items first :customers/name)}
:products (into {}
(for [{:keys [order_products/amount products/name]} items]
[(keyword name) amount]))})
(def orders-query
"SELECT o.id, o.notes, o.status, o.order_date, c.id, c.name, p.name, op.amount
FROM orders o JOIN customers c ON o.customer_id = c.id
JOIN order_products op ON o.id = op.order_id
JOIN products p on p.id = op.product_id ")
(defn- get-orders [tx where params]
(->> (into [(if where (str orders-query where) orders-query)] params)
(sql/query tx)
(group-by :orders/id)
vals
(map structure-order)))
(defn get-order [tx id]
(first (get-orders tx "WHERE o.id = ?" [id])))
(defn get-all [] (get-orders db/db-uri nil []))
(defn- orders-for-days [tx & days]
(let [days (remove nil? days)]
(->> days
(map t/inst->timestamp)
(map jdbc.types/as-date)
(get-orders tx (str "WHERE o.order_date::date IN " (db/psql-list days))))))
(defn- orders-between [tx from to]
(get-orders
tx
"WHERE o.order_date::date >= ? AND o.order_date::date <= ?"
[(some-> from t/inst->timestamp jdbc.types/as-date)
(some-> to t/inst->timestamp jdbc.types/as-date)]))
(defn replace! [{:keys [who products] :as order}]
(jdbc/with-transaction [tx db/db-uri]
(let [user-id (or (:id who)
(:customers/id (sql/get-by-id tx :customers (:name who) :name {})))
products-map (products/products-map tx products)
previous-day (some->> order :id (sql/get-by-id tx :orders) :orders/order_date (.toInstant))
order-id (upsert-order! tx user-id order)]
(sql/delete! tx :order_products {:order_id order-id})
(sql/insert-multi! tx :order_products
[:order_id :product_id :amount]
(for [[n amount] products
:let [product-id (-> n name products-map)]
:when product-id]
[order-id product-id amount]))
(orders-for-days tx previous-day (some-> order :day t/parse-date)))))
(defn delete! [id]
(jdbc/with-transaction [tx db/db-uri]
(let [day (some->> id (sql/get-by-id tx :orders) :orders/order_date (.toInstant))]
(sql/delete! tx :orders {:id id})
(when day (orders-for-days tx day)))))
(defn change-state!
"Update the state of the given order and also modify the number of products available:
* when `fulfilled` decrement the number of products
* when `waiting` increment the number (as this means a previously fulfilled order has been returned)"
[id state]
(jdbc/with-transaction [tx db/db-uri]
(let [order (get-order tx id)
operator (condp = state
"fulfilled" "-"
"waiting" "+")]
(when (not= (:state order) state)
(doseq [[prod amount] (:products order)]
(jdbc/execute-one! tx
[(str "UPDATE products SET amount = amount " operator " ? WHERE name = ?")
amount (name prod)]))
(sql/update! tx :orders {:status (jdbc.types/as-other state)} {:id id}))
(orders-for-days tx (-> order :day t/parse-date)))))

View File

@ -0,0 +1,33 @@
(ns chicken-master.products
(:require [next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
[chicken-master.db :as db]))
(defn get-all []
(prn "asd" (->> (sql/query db/db-uri ["select * from products where deleted is null"])
(map (fn [{:products/keys [name amount]}] [(keyword name) amount]))
(into {})))
(->> (sql/query db/db-uri ["select * from products where deleted is null"])
(map (fn [{:products/keys [name amount]}] [(keyword name) amount]))
(into {})))
(defn products-map [tx products]
(->> (map name (keys products))
(into [(str "SELECT id, name from products where name IN " (db/psql-list (keys products)))])
(sql/query tx)
(map #(vector (:products/name %) (:products/id %)))
(into {})))
(defn update! [new-products]
(prn new-products)
(jdbc/with-transaction [tx db/db-uri]
(doseq [[prod amount] new-products]
(jdbc/execute! tx
["INSERT INTO products (name, amount) VALUES(?, ?)
ON CONFLICT (name) DO UPDATE SET amount = EXCLUDED.amount, deleted = NULL"
(name prod) amount]))
(sql/update! tx :products
{:deleted true}
(into [(str "name NOT IN " (db/psql-list (keys new-products)))]
(->> new-products keys (map name)))))
(get-all))

View File

@ -8,7 +8,8 @@
(let [port (or (env :port) 3000)]
(run-jetty handler {:port port :join? false})))
(comment
(def h
(let [port (or (env :port) 3000)]
(run-jetty handler {:port port :join? false})))
(.stop h)
(.stop h))

View File

@ -0,0 +1,11 @@
(ns chicken-master.time
(:import [java.time Instant LocalDate ZoneOffset]
[java.sql Timestamp]))
(defn parse-date [date]
(-> date (LocalDate/parse) (.atStartOfDay) (.toInstant ZoneOffset/UTC)))
(defn inst->timestamp [inst] (Timestamp/from inst))
(defn now [] (Instant/now))

View File

@ -32,7 +32,6 @@
:editable-number-inputs (get-setting :editable-number-inputs false) ; only allow number modifications in the edit modal
:hide-fulfilled-orders (get-setting :hide-fulfilled-orders false)
:http-dispatch (get-setting :http-dispatch :http);-xhrio
:backend-url (get-setting :backend-url "http://localhost:3000/")
})
@ -83,12 +82,6 @@
(input :hide-fulfilled-orders "ukryj wydane zamówienia" {:type :checkbox})
[:h3 "Ustawienia tyłu"]
[:label {:for :http-dispatch} "re-frame http dispatcher"]
[:select {:id :http-dispatch :name :http-dispatch :value (settings :http-dispatch)
:on-change #(change-setting :http-dispatch (-> % .-target .-value keyword))}
[:option {:value :http} "client side mock"]
[:option {:value :http-xhrio} "re-frame-http-fx"]]
(input :backend-url "backend URL" {})
[:button {:on-click #(re-frame/dispatch

View File

@ -53,7 +53,7 @@
(re-frame/reg-event-fx
::remove-order
(fn [_ [_ id]]
{(settings :http-dispatch) (http-request :delete (str "orders/" id))}))
{:http-xhrio (http-request :delete (str "orders/" id))}))
(re-frame/reg-event-db
::failed-request
@ -67,7 +67,7 @@
(re-frame/reg-event-fx
::move-order
(fn [{{orders :orders start-date :start-date} :db} [_ id day]]
{(settings :http-dispatch)
{:http-xhrio
(http-request :put (str "orders/" id)
:body (-> id orders (assoc :day day :start-from start-date)))}))
@ -83,38 +83,44 @@
::fulfill-order
(fn [{db :db} [_ id]]
{:db (assoc-in db [:orders id :state] :pending)
(settings :http-dispatch) (http-request :post (str "orders/" id "/fulfilled"))}))
:http-xhrio (http-request :post (str "orders/" id "/fulfilled"))}))
(re-frame/reg-event-fx
::reset-order
(fn [{db :db} [_ id]]
{:db (assoc-in db [:orders id :state] :waiting)
(settings :http-dispatch) (http-request :post (str "orders/" id "/waiting"))}))
:http-xhrio (http-request :post (str "orders/" id "/waiting"))}))
(re-frame/reg-event-fx
::save-order
(fn [{{order :order-edit} :db} [_ form]]
{:dispatch [::hide-modal :order-edit]
(settings :http-dispatch) (http-post (str "orders")
:http-xhrio (http-post (str "orders")
(merge
(select-keys order [:id :day :hour :state])
(select-keys form [:id :day :hour :state :who :notes :products])))}))
(re-frame/reg-event-db
::process-fetched-days
(fn [db [_ days]]
(fn [db [_ orders]]
(prn orders)
(prn (:current-days db))
(prn
(let [days (group-by :day orders)]
(for [[day orders] (:current-days db)]
[day (if (contains? days day) (days day) orders)])))
(-> db
(assoc :loading? nil)
(update :current-days #(map (fn [[day orders]]
[day (if (contains? days day)
(days day) orders)]) %))
(update :orders #(reduce (fn [m cust] (assoc m (:id cust) cust)) % (-> days vals flatten))))))
(update :current-days (fn [current-days]
(let [days (group-by :day orders)]
(for [[day orders] current-days]
[day (if (contains? days day) (days day) orders)]))))
(update :orders #(reduce (fn [m cust] (assoc m (:id cust) cust)) % orders)))))
(re-frame/reg-event-fx
::scroll-weeks
(fn [{db :db} [_ offset]]
{:fx [;[:dispatch [::fetch-stock]]
[:dispatch [::start-loading]]
{:fx [[:dispatch [::start-loading]]
[:dispatch [::show-from-date (-> db
:start-date
time/parse-date
@ -136,7 +142,7 @@
::fetch-orders
(fn [_ [_ from to]]
{:dispatch [::start-loading]
(settings :http-dispatch) (http-get "orders" {} ::process-stock)}))
:http-xhrio (http-get "orders" {} ::process-stock)}))
;; Customers events
(re-frame/reg-event-fx
@ -148,14 +154,14 @@
(re-frame/reg-event-fx
::add-customer
(fn [_ [_ customer-name]]
{(settings :http-dispatch) (http-request :post "customers"
{:http-xhrio (http-request :post "customers"
:body {:name customer-name}
:on-success ::process-stock)}))
(re-frame/reg-event-fx
::remove-customer
(fn [_ [_ id]]
{:dispatch [::start-loading]
(settings :http-dispatch) (http-request :delete (str "customers/" id)
:http-xhrio (http-request :delete (str "customers/" id)
:on-success ::process-stock)}))
;;; Storage events
@ -170,7 +176,7 @@
::fetch-stock
(fn [_ _]
{:dispatch [::start-loading]
(settings :http-dispatch) (http-get "stock" {} ::process-stock)}))
:http-xhrio (http-get "stock" {} ::process-stock)}))
(defn assoc-if [coll key val] (if val (assoc coll key val) coll))
(re-frame/reg-event-fx
@ -179,7 +185,7 @@
{:db (-> db
(assoc-if :products products)
(assoc-if :customers customers)
(assoc-if :orders orders))
(assoc-if :orders (some->> orders (into {} (map #(vector (:id %) %))))))
:dispatch [::scroll-weeks 0]
}))
@ -188,7 +194,7 @@
(fn [_ [_ products]]
{:fx [[:dispatch [::hide-modal :stock]]
[:dispatch [::start-loading]]]
(settings :http-dispatch) (http-request :post "products" :body products :on-sucess ::process-stock)}))
:http-xhrio (http-request :post "products" :body products :on-sucess ::process-stock)}))
;; Settings
@ -217,38 +223,3 @@
{:fx [[:dispatch [::start-loading]]
[:dispatch [::fetch-stock]]
[:dispatch [::fetch-orders]]]}))
(re-frame/reg-fx
:http
(fn [{:keys [method uri params body on-success on-fail]}]
(condp = uri
"http://localhost:3000/stock" (re-frame/dispatch (conj on-success (mocks/fetch-stock params)))
"get-customers" (re-frame/dispatch (conj on-success (mocks/fetch-customers params)))
"add-customer" (re-frame/dispatch (conj on-success (mocks/add-customer params)))
"save-stock" (re-frame/dispatch (conj on-success (mocks/save-stocks body)))
(let [parts (clojure.string/split uri "/")]
(cond
(and (= method :get) (= uri "http://localhost:3000/orders"))
(re-frame/dispatch (conj on-success (mocks/fetch-orders params)))
(and (= method :post) (= uri "http://localhost:3000/orders"))
(re-frame/dispatch (conj on-success (mocks/replace-order nil (cljs.reader/read-string body))))
(and (= method :post) (= uri "http://localhost:3000/products"))
(re-frame/dispatch (conj on-success (mocks/save-stocks (cljs.reader/read-string body))))
(and (= method :delete) (= (nth parts 3) "orders"))
(re-frame/dispatch (conj on-success (mocks/delete-order (-> parts (nth 4) (js/parseInt)))))
(and (= method :delete) (= (nth parts 3) "customers"))
(re-frame/dispatch (conj on-success (mocks/delete-customer (-> parts (nth 4) (js/parseInt)))))
(and (= method :post) (= uri "http://localhost:3000/customers"))
(re-frame/dispatch (conj on-success (mocks/add-customer (cljs.reader/read-string body))))
(-> parts last #{"fulfilled" "waiting"})
(re-frame/dispatch (conj on-success (mocks/order-state {:id (-> parts (nth 4) (js/parseInt)) :state (keyword (last parts))})))
true (prn "unhandled" method uri)
))
)))