Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement user resolvers, db config, and transit config.
  • Loading branch information
codonnell committed Mar 31, 2020
1 parent 4bf3050 commit 20b7a91
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 33 deletions.
7 changes: 7 additions & 0 deletions Dockerfile.flyway
@@ -0,0 +1,7 @@
FROM flyway/flyway:6.3.2-alpine

COPY migrations /flyway/sql

ENTRYPOINT ["/bin/sh"]

CMD ["-c", "/flyway/flyway -url=jdbc:postgresql://${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} -user=${POSTGRES_USER} -password=${POSTGRES_PASSWORD} -connectRetries=60 migrate"]
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -8,6 +8,8 @@ In order to run this application, you need to have the following installed:
* [node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
* [java](https://adoptopenjdk.net/)
* [clojure cli](https://clojure.org/guides/getting_started)
* [docker](https://docs.docker.com/get-docker/)
* [docker compose](https://docs.docker.com/compose/install/)

With these installed, run
```bash
Expand All @@ -18,9 +20,23 @@ to install javascript dependencies.

## Running

### Database

We run a local postgres database inside docker-compose. To start the database, run
```bash
docker-compose up -d
```
After starting the database, you'll need to run migrations, which you can do with
```bash
./scripts/migrate-local.sh
```
There's also a convenience script available at `./scripts/psql` to open up a psql client connected to the database. There are resources to learn more about working with a database inside docker compose in the [documentation](https://docs.docker.com/compose/).

### Application

To run this application in development mode, start a shadow-cljs server with
```bash
npx shadow-cljs -d nrepl:0.7.0-beta1 -d cider/piggieback:0.4.2 -d refactor-nrepl:2.5.0 -d cider/cider-nrepl:0.25.0-SNAPSHOT server
npx shadow-cljs -d nrepl:0.7.0 -d cider/piggieback:0.4.2 -d refactor-nrepl:2.5.0 -d cider/cider-nrepl:0.25.0-SNAPSHOT server
```

With this running, you can control compilation by accessing the shadow-cljs server at http://localhost:9630. In addition, this command will start up an nrepl server, which you should connect to with your preferred REPL. Alternatively, CIDER users can run `cider-jack-in-clj&cljs`.
Expand Down
26 changes: 17 additions & 9 deletions deps.edn
@@ -1,21 +1,29 @@
{:paths ["src" "resources"]
:deps {com.fulcrologic/fulcro {:mvn/version "3.1.22"}
com.taoensso/timbre {:mvn/version "4.10.0"}
ring/ring-core {:mvn/version "1.8.0"}
ring/ring-defaults {:mvn/version "0.3.2"}
bk/ring-gzip {:mvn/version "0.3.0"}
mount {:mvn/version "0.1.16"}
aero {:mvn/version "1.1.5"}
http-kit {:mvn/version "2.4.0-alpha6"}}
:deps {com.fulcrologic/fulcro {:mvn/version "3.1.22"}
com.wsscode/pathom {:mvn/version "2.2.31"}
edn-query-language/eql {:mvn/version "0.0.9"}
com.taoensso/timbre {:mvn/version "4.10.0"}
com.cognitect/transit-clj {:mvn/version "1.0.324"}
ring/ring-core {:mvn/version "1.8.0"}
ring/ring-defaults {:mvn/version "0.3.2"}
bk/ring-gzip {:mvn/version "0.3.0"}
mount {:mvn/version "0.1.16"}
seancorfield/next.jdbc {:mvn/version "1.0.409"}
honeysql {:mvn/version "0.9.10"}
nilenso/honeysql-postgres {:mvn/version "0.2.6"}
org.postgresql/postgresql {:mvn/version "42.2.11"}
hikari-cp {:mvn/version "2.11.0"}
aero {:mvn/version "1.1.6"}
http-kit {:mvn/version "2.4.0-alpha6"}}
:aliases {:dev {:extra-paths ["dev"]
:jvm-opts ["-Dtrace"]
:extra-deps {org.clojure/tools.namespace {:mvn/version "0.3.1"}
org.clojure/clojurescript {:mvn/version "1.10.597"}
com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"}
org.clojure/core.async {:mvn/version "1.0.567"}
com.cognitect/transit-cljs {:mvn/version "0.8.256"}
com.wsscode/async {:mvn/version "1.0.2"}
clj-commons/pushy {:mvn/version "0.3.10"}
edn-query-language/eql {:mvn/version "0.0.9"}
thheller/shadow-cljs {:mvn/version "2.8.83"}
binaryage/devtools {:mvn/version "0.9.10"}}}
:outdated {:extra-deps {olical/depot {:mvn/version "1.8.4"}}
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yaml
@@ -0,0 +1,13 @@
version: "3.7"
services:
postgres:
image: postgres:12.2-alpine
restart: always
environment:
POSTGRES_PASSWORD: password
ports:
- "15432:5432"
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
8 changes: 8 additions & 0 deletions migrations/V1__CreateUserTable.sql
@@ -0,0 +1,8 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;

CREATE TABLE "user" (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE,
auth0_id text NOT NULL UNIQUE,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
8 changes: 7 additions & 1 deletion resources/config.edn
@@ -1 +1,7 @@
{:port #long #or [#env PORT 3000]}
{:database-spec {:username #or [#env POSTGRES_USER "postgres"]
:password #or [#env POSTGRES_PASSWORD "password"]
:server-name #or [#env POSTGRES_HOSTNAME "localhost"]
:port-number #long #or [#env POSTGRES_PORT 15432]
:database-name #or [#env POSTGRES_DB "postgres"]
:sslmode #or [#env POSTGRES_SSLMODE "disable"]}
:port #long #or [#env PORT 3000]}
18 changes: 18 additions & 0 deletions scripts/migrate-local.sh
@@ -0,0 +1,18 @@
#!/bin/bash

pushd "$(git rev-parse --show-toplevel)"

docker build \
-f Dockerfile.flyway \
-t cpodonnell/mygiftlist-blog:migrate-local .

docker run --rm \
--network mygiftlist-blog_default \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_HOSTNAME=postgres \
-e POSTGRES_PORT=5432 \
-e POSTGRES_DB=postgres \
cpodonnell/mygiftlist-blog:migrate-local

popd
3 changes: 3 additions & 0 deletions scripts/psql
@@ -0,0 +1,3 @@
#!/bin/bash

docker-compose exec -u postgres postgres psql
15 changes: 12 additions & 3 deletions src/rocks/mygiftlist/application.cljs
@@ -1,7 +1,16 @@
(ns rocks.mygiftlist.application
(:require [com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.rendering.keyframe-render2 :as keyframe-render2]
[com.fulcrologic.fulcro.networking.http-remote :as http-remote]))
[com.fulcrologic.fulcro.networking.http-remote :as http-remote]
[rocks.mygiftlist.transit :as transit]))

(defonce SPA (app/fulcro-app {:optimized-render! keyframe-render2/render!
:remotes {:remote (http-remote/fulcro-http-remote {})}}))
(defonce SPA
(app/fulcro-app
{:optimized-render! keyframe-render2/render!
:remotes {:remote (http-remote/fulcro-http-remote
{:request-middleware
(http-remote/wrap-fulcro-request
identity transit/write-handlers)
:response-middleware
(http-remote/wrap-fulcro-response
identity transit/read-handlers)})}}))
9 changes: 7 additions & 2 deletions src/rocks/mygiftlist/client.cljs
Expand Up @@ -2,7 +2,10 @@
(:require
[rocks.mygiftlist.application :refer [SPA]]
[rocks.mygiftlist.authentication :as auth]
[rocks.mygiftlist.model.user :as m.user]
[rocks.mygiftlist.type.user :as user]
[com.fulcrologic.fulcro.algorithms.normalized-state :refer [swap!->]]
[com.fulcrologic.fulcro.algorithms.tempid :as tempid]
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
Expand Down Expand Up @@ -194,7 +197,9 @@
(do (comp/transact! SPA
[(route-to {:path (url->path js/window.location.pathname)})])
(let [{:keys [sub email]} (<! (auth/get-user-info))]
(comp/transact! SPA [(set-current-user
#:user{:id sub :email email})])))
(comp/transact! SPA [(m.user/set-current-user
#::user{:id (tempid/tempid)
:auth0-id sub
:email email})])))
(comp/transact! SPA
[(route-to {:path (dr/path-to LoginForm)})])))))
2 changes: 2 additions & 0 deletions src/rocks/mygiftlist/config.clj
Expand Up @@ -4,4 +4,6 @@

(def ^:private config (aero/read-config (io/resource "config.edn")))

(def database-spec (:database-spec config))

(def port (:port config))
103 changes: 103 additions & 0 deletions src/rocks/mygiftlist/db.clj
@@ -0,0 +1,103 @@
(ns rocks.mygiftlist.db
(:require [rocks.mygiftlist.config :as config]
[mount.core :refer [defstate]]
[hikari-cp.core :as pool]
[next.jdbc :as jdbc]
[next.jdbc.result-set :as result-set]
[next.jdbc.prepare :as p]
[clojure.string :as str]
[honeysql.core :as sql]
honeysql-postgres.format))

(def datasource-options
(merge {:auto-commit true
:read-only false
:connection-timeout 30000
:validation-timeout 5000
:idle-timeout 600000
:max-lifetime 1800000
:minimum-idle 10
:maximum-pool-size 10
:pool-name "db-pool"
:adapter "postgresql"
:register-mbeans false}
config/database-spec))

(defstate pool
:start (pool/make-datasource datasource-options)
:stop (pool/close-datasource pool))

(defn- qualify
"Given a kebab-case database table name, returns the namespace that
attributes coming from that table should have."
[table]
(when (seq table)
(str "rocks.mygiftlist.type." table)))

(defn- snake->kebab [s]
(str/replace s #"_" "-"))

(defn- as-qualified-kebab-maps [rs opts]
(result-set/as-modified-maps rs
(assoc opts
:qualifier-fn (comp qualify snake->kebab)
:label-fn snake->kebab)))

(def ^:private query-opts {:builder-fn as-qualified-kebab-maps})

(defn execute! [conn sql-map]
(jdbc/execute! conn
(sql/format sql-map :quoting :ansi)
query-opts))

(defn execute-one! [conn sql-map]
(jdbc/execute-one! conn
(sql/format sql-map :quoting :ansi)
query-opts))

(extend-protocol result-set/ReadableColumn

;; Automatically convert java.sql.Array into clojure vector in query
;; results
java.sql.Array
(read-column-by-label ^clojure.lang.PersistentVector
[^java.sql.Array v _]
(vec (.getArray v)))
(read-column-by-index ^clojure.lang.PersistentVector
[^java.sql.Array v _2 _3]
(vec (.getArray v)))

;; Output java.time.LocalDate instead of java.sql.Date in query
;; results
java.sql.Date
(read-column-by-label ^java.time.LocalDate
[^java.sql.Date v _]
(.toLocalDate v))
(read-column-by-index ^java.time.LocalDate
[^java.sql.Date v _2 _3]
(.toLocalDate v))

;; Output java.time.Instant instead of java.sql.Timestamp in query
;; results
java.sql.Timestamp
(read-column-by-label ^java.time.Instant
[^java.sql.Timestamp v _]
(.toInstant v))
(read-column-by-index ^java.time.Instant
[^java.sql.Timestamp v _2 _3]
(.toInstant v)))


(extend-protocol p/SettableParameter

;; Accept java.time.Instant as a query param
java.time.Instant
(set-parameter
[^java.time.Instant v ^java.sql.PreparedStatement ps ^long i]
(.setTimestamp ps i (java.sql.Timestamp/from v)))

;; Accept java.time.LocalDate as a query param
java.time.LocalDate
(set-parameter
[^java.time.LocalDate v ^java.sql.PreparedStatement ps ^long i]
(.setTimestamp ps i (java.sql.Timestamp/valueOf (.atStartOfDay v)))))
34 changes: 22 additions & 12 deletions src/rocks/mygiftlist/model/user.clj
@@ -1,24 +1,34 @@
(ns rocks.mygiftlist.model.user
(:require
[com.wsscode.pathom.connect :as pc :refer [defresolver defmutation]]
[rocks.mygiftlist.db :as db]
[rocks.mygiftlist.type.user :as user]))

(defonce users (atom {}))

(defresolver user-by-id [env {::user/keys [id]}]
(defresolver user-by-id
[{::db/keys [pool]} {::user/keys [id]}]
{::pc/input #{::user/id}
::pc/output [::user/email]}
(get @users id))
::pc/output [::user/id ::user/email ::user/auth0-id ::user/created-at]}
(db/execute-one! pool
{:select [:id :email :auth0_id :created_at]
:from [:user]
:where [:= id :id]}))

(defn- assign-tempid [{::user/keys [id] :as user} tempid]
(assoc user :tempids {tempid id}))

(defmutation insert-user [env {::user/keys [id] :as user}]
{::pc/params #{::user/id ::user/email}
(defmutation insert-user
[{::db/keys [pool]} {::user/keys [id auth0-id email] :as user}]
{::pc/params #{::user/email ::user/auth0-id}
::pc/output [::user/id]}
(swap! users assoc id user))
(cond-> (db/execute-one! pool
{:insert-into :user
:values [{:auth0_id auth0-id
:email email}]
:upsert {:on-conflict [:auth0_id]
:do-update-set [:email]}
:returning [:id]})
id (assign-tempid id)))

(def user-resolvers
[user-by-id
insert-user])

(comment
@users
)
18 changes: 18 additions & 0 deletions src/rocks/mygiftlist/model/user.cljs
@@ -0,0 +1,18 @@
(ns rocks.mygiftlist.model.user
(:require
[rocks.mygiftlist.type.user :as user]
[edn-query-language.core :as eql]
[com.fulcrologic.fulcro.algorithms.normalized-state :refer [swap!->]]
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]))

(defmutation set-current-user [{::user/keys [id auth0-id email]
:as user}]
(action [{:keys [state]}]
(swap!-> state
(assoc-in [:component/id :current-user] [::user/id id])
(assoc-in [::user/id id] user)))
(remote [_]
(eql/query->ast1 `[(insert-user
#::user{:id ~id
:auth0-id ~auth0-id
:email ~email})])))
10 changes: 7 additions & 3 deletions src/rocks/mygiftlist/parser.clj
Expand Up @@ -5,6 +5,7 @@
[com.wsscode.pathom.connect :as pc :refer [defresolver]]
[com.wsscode.pathom.core :as p]
[clojure.core.async :refer [<!!]]
[rocks.mygiftlist.db :as db]
[rocks.mygiftlist.type.user :as user]
[rocks.mygiftlist.model.user :as m.user]))

Expand Down Expand Up @@ -57,7 +58,8 @@
;; things to the resolver/mutation
;; environment, like the server config,
;; database connections, etc.
env))
(assoc env
::db/pool db/pool)))
(preprocess-parser-plugin log-requests)
p/error-handler-plugin
p/request-cache-plugin
Expand All @@ -74,6 +76,8 @@
tx))))))

(comment
(parser {} `[(m.user/insert-user #::user{:id 1 :email "me@example.com"})])
(parser {} [{[::user/id 2] [::user/id ::user/email]}])
(parser {} `[{(m.user/insert-user #::user{:auth0-id "auth0|abc123" :email "me@example.com"})
[::user/id]}])
(parser {} [{[::user/id #uuid "8d5c93d3-7d22-4925-bc66-118e5e3d7238"]
[::user/id ::user/auth0-id ::user/email]}])
)

0 comments on commit 20b7a91

Please sign in to comment.