├── .gitignore ├── README.md ├── config ├── dev │ └── config.edn └── test │ └── config.edn ├── deps.edn ├── env └── dev │ └── user.clj ├── src └── grok │ ├── config.clj │ ├── core.clj │ └── db │ ├── cards.clj │ ├── core.clj │ ├── decks.clj │ ├── schema.clj │ └── users.clj └── test └── grok ├── core_test.clj └── db ├── cards_test.clj ├── core_test.clj ├── decks_test.clj ├── users_test.clj └── with_db.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | 3 | #uberjar stuff 4 | target 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grok 2 | 3 | Grok is a learning tool that allows users to learn different facts about different topics 4 | 5 | In this tutorial series we will be builing a limited version of Anki, using the following technologies: 6 | 7 | - Clojure 8 | - Datomic 9 | - ClojureScript 10 | - Re-Frame 11 | - DataScript (via Re-Posh) 12 | 13 | # Information Model 14 | 15 | ## User 16 | - id (uuid) 17 | - full-name (string) 18 | - username (string) 19 | - email (string => unique) 20 | - password (string => hashed) 21 | - token (string) 22 | 23 | ## Decks 24 | - id (uuid) 25 | - author (Ref) 26 | - title (string) 27 | - tags (vector of strings) 28 | 29 | ## Cards 30 | - id (uuid) 31 | - deck (REF) 32 | - front (string) 33 | - back (string) 34 | - progress (long) 35 | - next-study-date (date|instant) 36 | 37 | # HTTP Rest End Points 38 | 39 | Now that we are aware of our data model, lets look at how we will design/structure our rest endpoints 40 | 41 | ## Auth 42 | /api/login 43 | - POST => login a user 44 | /api/register 45 | - POST => register a user 46 | 47 | ## Users 48 | /api/users/:user-id 49 | - GET => get a single user by ID 50 | - PUT => Update a user 51 | - DELETE => Delete a single user 52 | 53 | /api/users 54 | - POST => Post a new user 55 | 56 | ## Decks 57 | /api/users/:user-id/decks/:deck-id 58 | - GET => get a single deck by ID belonging to a certain user 59 | - PUT => Update a deck 60 | - DELETE => Delete a deck 61 | 62 | /api/users/:user-id/decks 63 | - POST => Post a new deck 64 | - GET => Browse a list of decks 65 | 66 | ## Cards 67 | /api/users/:user-id/decks/:deck-id/cards/:card-id 68 | - GET => get a single cards by ID belonging to a certain deck 69 | - PUT => Update a card 70 | - DELETE => Delete a card 71 | 72 | /api/users/:user-id/decks/:deck-id/cards 73 | - POST => Post a new card 74 | - GET => Browse a list of cards 75 | 76 | --- 77 | 78 | # Step: 1 Data Layer - DONE 79 | 80 | ## User 81 | - Create (done) 82 | - Read (done) 83 | - Update (done) 84 | - Delete (done) 85 | 86 | ## Decks 87 | - List (done) 88 | - Read (done) 89 | - Create (done) 90 | - Update (done) 91 | - Delete (done) 92 | 93 | ## Cards 94 | - List 95 | - Read 96 | - Create 97 | - Update 98 | - Delete 99 | --- 100 | 101 | Before we go ahead and implement CRUD functions for Decks and Cards, lets talk about run time state 102 | 103 | What is run time state? 104 | Environment variables, database connection are considered run time state. 105 | - Run time state can dependent on each other. For example in order to create database connection, you need database-uri, which comes from the env variables. 106 | - Currently our application only has two run time state. 107 | - As our application grows, so will our run time state. 108 | - Since more state => more complexity. We need to make sure to tame it 109 | 110 | Luckily clojure provides with libraries such as component, mount, integrant to manage these run time state. 111 | 112 | In this case, I will be using mount. 113 | 114 | Mount has two basic function 115 | 1. start -> which starts the state 116 | 2. stop -> stop which stops the state 117 | 118 | You can also decided which resource to start, or which resources to omit when starting mount for example 119 | 120 | In order to use convert run time state/resource -> mount state, we have to use the macro defstate provided by mount. 121 | 122 | Another benefit mount provide is that it enables reloaded workflow... lets much easier to explain with code... so lets get started with mount 123 | 124 | ## Step 1: Install dependency 125 | 126 | ## Step 2: Restart REPL to install dependency so that we can use it 127 | 128 | ## Step 3: Start using mount 129 | 130 | ## Step 4: Use mount start function to start life cycle of the application 131 | 132 | We will install one more resource to start namespaces of our application 133 | --- 134 | # Step - 2 Data Transport Layer - HTTP 135 | 136 | In next video we will start with the server series. Thank you so much for watching and for all the feedbacks. 137 | First step was an experimental with music and coding (Data Layer) 138 | Second step I will go over the code line by line. 139 | 140 | Thanks for watching :D 141 | -------------------------------------------------------------------------------- /config/dev/config.edn: -------------------------------------------------------------------------------- 1 | {:database-uri "datomic:sql://grok-development?jdbc:postgresql://localhost:5432/datomic?user=datomic&password=datomic" 2 | :port 8080} 3 | -------------------------------------------------------------------------------- /config/test/config.edn: -------------------------------------------------------------------------------- 1 | {:port 9090} 2 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src"] 3 | :deps 4 | {org.clojure/clojure {:mvn/version "1.10.1"} 5 | com.datomic/datomic-pro {:mvn/version "0.9.6014"} 6 | org.postgresql/postgresql {:mvn/version "9.3-1102-jdbc41"} 7 | http-kit {:mvn/version "2.4.0"} 8 | metosin/reitit {:mvn/version "0.5.5"} 9 | yogthos/config {:mvn/version "1.1.7"} 10 | mount {:mvn/version "0.1.16"}} 11 | :aliases 12 | {:server {:main-opts ["-m" "grok.core"]} 13 | :dev {:extra-paths ["config/dev" "env/dev"] 14 | :extra-deps {org.clojure/tools.namespace {:mvn/version "1.0.0"}}} 15 | :test {:extra-paths ["test" "config/test"] 16 | :extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"} 17 | lambdaisland/kaocha-cloverage {:mvn/version "1.0.63"}} 18 | :main-opts ["-m" "kaocha.runner"]} 19 | :socket-repl {:jvm-opts ["-Dclojure.server.repl={:port,50505,:accept,clojure.core.server/repl}"]}}} 20 | -------------------------------------------------------------------------------- /env/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [mount.core :as mount] 3 | [clojure.tools.namespace.repl :as tn] 4 | [grok.core])) 5 | 6 | (defn refresh-ns 7 | "Refresh/reloads all the namespace" 8 | [] 9 | (tn/refresh-all)) 10 | 11 | (defn start 12 | "Mount starts life cycle of runtime state" 13 | [] 14 | (mount/start)) 15 | 16 | (defn stop 17 | "Mount stops life cycle of runtime state" 18 | [] 19 | (mount/stop)) 20 | 21 | (defn restart-dev 22 | [] 23 | (stop) 24 | (refresh-ns) 25 | (start)) 26 | 27 | (comment 28 | (restart-dev)) 29 | -------------------------------------------------------------------------------- /src/grok/config.clj: -------------------------------------------------------------------------------- 1 | (ns grok.config 2 | (:require [mount.core :refer [defstate]] 3 | [config.core :as config])) 4 | 5 | ;; Lets define our first state 6 | (defstate env 7 | :start config/env) 8 | -------------------------------------------------------------------------------- /src/grok/core.clj: -------------------------------------------------------------------------------- 1 | (ns grok.core 2 | (:require [grok.db.core])) 3 | 4 | (defn -main [] 5 | (println "Hello grok")) 6 | -------------------------------------------------------------------------------- /src/grok/db/cards.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.cards 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [datomic.api :as d] 5 | [clojure.test.check.generators :as gen])) 6 | 7 | ;; Step 1 - Transact cards schema - DONE 8 | ;; Step 2 - Create cards spec - DONE 9 | (s/def :card/id uuid?) 10 | (s/def :card/front string?) 11 | (s/def :card/back string?) 12 | (s/def :card/progress (s/and #(> % 0) int?)) 13 | (s/def :card/next-study-date inst?) 14 | 15 | (s/def ::card 16 | (s/keys :req [:card/front :card/back] 17 | :opt [:card/id :card/progress :card/next-study-date])) 18 | 19 | ;; Lets try to generate random card using generate function 20 | ;; (gen/generate (s/gen ::card)) 21 | 22 | (defn browse 23 | "list all the cards belonging to a certain deck" 24 | [db deck-id] 25 | (d/q '[:find [(pull ?cards [*]) ...] 26 | :in $ ?deck-id 27 | :where 28 | [?deck :deck/id ?deck-id] 29 | [?cards :card/deck ?deck]] 30 | db deck-id)) 31 | 32 | ; - Read - fetch a single card by id - DONE 33 | (defn fetch 34 | "Fetch a single card by ID, return nil if not found" 35 | [db deck-id card-id] 36 | (d/q '[:find (pull ?card [*]) . 37 | :in $ ?deck-id ?card-id 38 | :where 39 | [?deck :deck/id ?deck-id] 40 | [?card :card/id ?card-id] 41 | [?card :card/deck ?deck]] 42 | db deck-id card-id)) 43 | ; - Create - create a new card - DONE 44 | (defn create! 45 | "Create a new card" 46 | [conn deck-id card-params] 47 | (if (s/valid? ::card card-params) 48 | (let [card-id (d/squuid) 49 | tx-data (-> card-params 50 | (assoc :card/deck [:deck/id deck-id]) 51 | (assoc :card/id card-id))] 52 | (d/transact conn [tx-data]) 53 | card-id) 54 | (throw (ex-info "Card is invalid" 55 | {:grok/error-id :validation 56 | :error "Invalid card input values"})))) 57 | ; - Update - update a card - DONE 58 | ;; Edit function takes three params 59 | ;; - conn - datomic connection 60 | ;; - deck-id - id of the deck 61 | ;; - card-id - id of the card 62 | ;; - card-parans - updated card values 63 | ;; Lets write the failing test first 64 | ;; Implementation 65 | ;; - check if card with card-id and deck-id exists (use fetch function) 66 | ;; - transact updated params (transact function) 67 | ;; - return the updated-card (q function) 68 | ;; - Lets see if the tests pass 69 | ;; - looks like edit functionality works for now 70 | (defn edit! 71 | "Editing an existing card" 72 | [conn deck-id card-id card-params] 73 | (when (fetch (d/db conn) deck-id card-id) 74 | (let [tx-data (-> card-params 75 | (assoc :card/id card-id)) 76 | db-after (:db-after @(d/transact conn [tx-data]))] 77 | (fetch db-after deck-id card-id)))) 78 | 79 | ; - Delete - delete a card - DONE 80 | ;; We will copy /paste the implementation from deck for deleting entity 81 | ;; We will refactor later in the series 82 | (defn delete! [conn deck-id card-id] 83 | (when-let [card (fetch (d/db conn) deck-id card-id)] ;; - 1 84 | (d/transact conn [[:db/retractEntity [:card/id card-id]]]) ;; - 2 85 | card)) ;; - 3 86 | 87 | ;; 1 - we try to see if we have the card, only if the card exists 88 | ;; 2 - we use the retractEntity function from datomic to retract the card entity 89 | ;; 3 - lastly we return the deleted card itself 90 | 91 | ;; lets see if the tests pass 92 | ;; Looks like all the tests pass 93 | ;; Finally we have implemented all the crud operations for user deck and card 94 | ;; Lets revisit the documentation 95 | -------------------------------------------------------------------------------- /src/grok/db/core.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.core 2 | (:require [datomic.api :as d] 3 | [grok.config :refer [env]] 4 | [mount.core :as mount :refer [defstate]] 5 | [grok.db.schema :refer [schema]])) 6 | 7 | (defn create-conn [db-uri] 8 | (when db-uri 9 | (d/create-database db-uri) 10 | (let [conn (d/connect db-uri)] 11 | conn))) 12 | 13 | ;; Lets change our conn to mount state 14 | (defstate conn 15 | :start (create-conn (:database-uri env)) 16 | :stop (.release conn)) 17 | 18 | ;; Schema transaction 19 | (comment 20 | (def tx @(d/transact conn schema))) 21 | -------------------------------------------------------------------------------- /src/grok/db/decks.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.decks 2 | (:require 3 | [datomic.api :as d] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test.check.generators :as gen] 6 | [grok.db.core :refer [conn]])) 7 | 8 | ;; Deck Spec 9 | (s/def :deck/id uuid?) 10 | (s/def :deck/title (s/and string? #(seq %))) 11 | (s/def :deck/tags (s/coll-of string? :kind vector? :min-count 1)) 12 | 13 | (s/def ::deck 14 | (s/keys 15 | :req [:deck/title :deck/tags] 16 | :opt [:deck/id])) 17 | 18 | (defn browse 19 | "Browse a list of decks, blonging to a certain user, returns 20 | empty vector if user has not created any deck" 21 | [db user-id] 22 | (d/q '[:find [(pull ?deck [*]) ...] 23 | :in $ ?uid 24 | :where 25 | [?user :user/id ?uid] 26 | [?deck :deck/author ?user]] 27 | db user-id)) 28 | 29 | ; - Read 30 | ;; passing . after find caluse returns a single item 31 | ;; small typo 32 | (defn fetch 33 | "Fetch a single deck by ID, returns nil if not found" 34 | [db user-id deck-id] 35 | (d/q '[:find (pull ?deck [*]) . 36 | :in $ ?uid ?did 37 | :where 38 | [?user :user/id ?uid] 39 | [?deck :deck/id ?did] 40 | [?deck :deck/author ?user]] 41 | db user-id deck-id)) 42 | 43 | ; - Create 44 | (defn create! 45 | "Create a new deck" 46 | [conn user-id deck-params] 47 | (if (s/valid? ::deck deck-params) 48 | (let [deck-id (d/squuid) 49 | tx-data (merge deck-params {:deck/author [:user/id user-id] 50 | :deck/id deck-id})] 51 | (d/transact conn [tx-data]) 52 | deck-id) 53 | (throw (ex-info "Deck is invalid" 54 | {:grok/error-id :validation 55 | :error "Invalid deck input values"})))) 56 | ; - Update 57 | (defn edit! 58 | "Edit an existing deck" 59 | [conn user-id deck-id deck-params] 60 | (if (fetch (d/db conn) user-id deck-id) 61 | (let [tx-data (merge deck-params {:deck/id deck-id}) 62 | db-after (:db-after @(d/transact conn [tx-data]))] 63 | (fetch db-after user-id deck-id)) 64 | (throw (ex-info "Unable to update deck" 65 | {:grok/error-id :server-error 66 | :error "Unable to update deck"})))) 67 | 68 | ; - Delete 69 | (defn delete! 70 | "Delete a deck" 71 | [conn user-id deck-id] 72 | (when-let [deck (fetch (d/db conn) user-id deck-id)] 73 | (d/transact conn [[:db/retractEntity [:deck/id deck-id]]]) 74 | deck)) 75 | -------------------------------------------------------------------------------- /src/grok/db/schema.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.schema) 2 | 3 | (def schema 4 | [{:db/ident :user/id 5 | :db/valueType :db.type/uuid 6 | :db/cardinality :db.cardinality/one 7 | :db/unique :db.unique/identity 8 | :db/doc "ID of the User"} 9 | {:db/ident :user/email 10 | :db/valueType :db.type/string 11 | :db/cardinality :db.cardinality/one 12 | :db/unique :db.unique/identity 13 | :db/doc "Email of the User"} 14 | {:db/ident :user/username 15 | :db/valueType :db.type/string 16 | :db/cardinality :db.cardinality/one 17 | :db/doc "Username of the User"} 18 | {:db/ident :user/password 19 | :db/valueType :db.type/string 20 | :db/cardinality :db.cardinality/one 21 | :db/doc "Hashed Password of the User"} 22 | {:db/ident :user/token 23 | :db/valueType :db.type/string 24 | :db/cardinality :db.cardinality/one 25 | :db/doc "Token of the User"} 26 | {:db/ident :deck/id 27 | :db/valueType :db.type/uuid 28 | :db/cardinality :db.cardinality/one 29 | :db/unique :db.unique/identity 30 | :db/doc "ID of the deck"} 31 | {:db/ident :deck/author 32 | :db/valueType :db.type/ref 33 | :db/cardinality :db.cardinality/one 34 | :db/doc "Author of the deck"} 35 | {:db/ident :deck/title 36 | :db/valueType :db.type/string 37 | :db/cardinality :db.cardinality/one 38 | :db/doc "Title of the deck"} 39 | {:db/ident :deck/tags 40 | :db/valueType :db.type/string 41 | :db/cardinality :db.cardinality/many 42 | :db/doc "Tags of the deck"} 43 | {:db/ident :card/id 44 | :db/valueType :db.type/uuid 45 | :db/cardinality :db.cardinality/one 46 | :db/unique :db.unique/identity 47 | :db/doc "ID of the card"} 48 | {:db/ident :card/deck 49 | :db/valueType :db.type/ref 50 | :db/cardinality :db.cardinality/one 51 | :db/doc "Deck ID of the card"} 52 | {:db/ident :card/front 53 | :db/valueType :db.type/string 54 | :db/cardinality :db.cardinality/one 55 | :db/doc "Front Content of the card"} 56 | {:db/ident :card/back 57 | :db/valueType :db.type/string 58 | :db/cardinality :db.cardinality/one 59 | :db/doc "Back Content of the card"} 60 | {:db/ident :card/progress 61 | :db/valueType :db.type/long 62 | :db/cardinality :db.cardinality/one 63 | :db/doc "Progress Point of the card"} 64 | {:db/ident :card/next-study-date 65 | :db/valueType :db.type/instant 66 | :db/cardinality :db.cardinality/one 67 | :db/doc "Next study date of the card"}]) 68 | -------------------------------------------------------------------------------- /src/grok/db/users.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.users 2 | (:require [datomic.api :as d] 3 | [grok.db.core :refer [conn]] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test.check.generators :as gen])) 6 | 7 | (defn validate-email [email] 8 | (let [email-regex #"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"] 9 | (re-matches email-regex email))) 10 | 11 | (s/def :user/email 12 | (s/with-gen 13 | (s/and string? validate-email) 14 | #(s/gen #{"john@gmail.com" "jane@me.com"}))) 15 | (s/def :user/password 16 | (s/with-gen 17 | (s/and string? #(> (count %) 6)) 18 | #(s/gen #{"abcdeffas" "asdasdasd231da" "asdas3wa2ed"}))) 19 | 20 | (s/def :user/username 21 | (s/with-gen 22 | string? 23 | #(s/gen #{"john.doe" "mark.dijk" "mo.salah"}))) 24 | 25 | (s/def :user/token 26 | (s/with-gen 27 | string? 28 | #(s/gen #{"abcdeff" "asdasdasd231da" "asdas3wa2ed"}))) 29 | 30 | (s/def :user/id uuid?) 31 | 32 | (s/def ::user 33 | (s/keys :req [:user/email :user/password] 34 | :opt [:user/id :user/token :user/username])) 35 | 36 | (defn create! [conn user-params] 37 | (if (s/valid? ::user user-params) 38 | (let [user-id (d/squuid) 39 | tx-data (merge user-params {:user/id user-id})] 40 | (d/transact conn [tx-data]) 41 | user-id) 42 | (throw (ex-info "User is invalid" 43 | {:grok/error-id :validation 44 | :error "Invalid email or password provided"})))) 45 | 46 | (defn fetch 47 | ([db user-id] 48 | (fetch db user-id '[*])) 49 | ([db user-id pattern] 50 | (d/q '[:find (pull ?uid pattern) . 51 | :in $ ?user-id pattern 52 | :where 53 | [?uid :user/id ?user-id]] 54 | db user-id pattern))) 55 | 56 | (defn edit! 57 | [conn user-id user-params] 58 | (if (fetch (d/db conn) user-id) 59 | (let [tx-data (merge user-params {:user/id user-id}) 60 | db-after (:db-after @(d/transact conn [tx-data]))] 61 | (fetch db-after user-id)) 62 | (throw (ex-info "Unable to update user" 63 | {:grok/error-id :server-error 64 | :error "Unable to edit user"})))) 65 | 66 | (defn delete! 67 | [conn user-id] 68 | (when-let [user (fetch (d/db conn) user-id)] 69 | (d/transact conn [[:db/retractEntity [:user/id user-id]]]) 70 | user)) 71 | -------------------------------------------------------------------------------- /test/grok/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns grok.core-test 2 | (:require [clojure.test :refer [deftest testing is]])) 3 | 4 | (deftest sample 5 | (testing "1 + 1 = 2" 6 | (is (= 2 (+ 1 1))))) 7 | -------------------------------------------------------------------------------- /test/grok/db/cards_test.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.cards-test 2 | (:require [clojure.test :refer [is deftest testing use-fixtures]] 3 | [datomic.api :as d] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test.check.generators :as gen] 6 | [grok.db.cards :as SUT] 7 | [grok.db.decks :as decks] 8 | [grok.db.with-db :refer [with-db *conn*]])) 9 | 10 | ;; Invoke use fixtures so that new db is created for every test 11 | (use-fixtures :each with-db) 12 | 13 | (deftest cards 14 | (let [user-id (:user/id (d/entity (d/db *conn*) [:user/email "test@test.com"]))] 15 | (testing "browse - returns empty vector if the user has not created any card for the deck" 16 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 17 | deck-id (decks/create! *conn* user-id new-deck) 18 | cards (SUT/browse (d/db *conn*) deck-id)] 19 | (is (empty? cards)) 20 | (is (vector? cards)))) 21 | (testing "browse - returns a list of cards for the deck" 22 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 23 | deck-id (decks/create! *conn* user-id new-deck) 24 | new-card {:card/id (d/squuid) 25 | :card/deck [:deck/id deck-id] 26 | :card/front "What is Cloujure" 27 | :card/back "A programming language"}] 28 | @(d/transact *conn* [new-card]) 29 | (let [cards (SUT/browse (d/db *conn*) deck-id) 30 | card (first cards)] 31 | (is (seq cards)) 32 | (is (s/valid? ::SUT/card card)) 33 | (is (vector? cards))))) 34 | 35 | (testing "fetch - returns a card by ID" 36 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 37 | deck-id (decks/create! *conn* user-id new-deck) 38 | card-id (d/squuid) 39 | new-card {:card/id card-id 40 | :card/deck [:deck/id deck-id] 41 | :card/front "What is Cloujure" 42 | :card/back "A programming language"}] 43 | @(d/transact *conn* [new-card]) 44 | (let [card (SUT/fetch (d/db *conn*) deck-id card-id)] 45 | (is (s/valid? ::SUT/card card))))) 46 | (testing "fetch - returns a nil if not found" 47 | (let [card (SUT/fetch (d/db *conn*) 1 2)] 48 | (is (nil? card)))) 49 | (testing "create! - Creates a new card and returns the card ID" 50 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 51 | deck-id (decks/create! *conn* user-id new-deck) 52 | new-card {:card/deck [:deck/id deck-id] 53 | :card/front "What is Cloujure" 54 | :card/back "A programming language"} 55 | card-id (SUT/create! *conn* deck-id new-card)] 56 | (is (uuid? card-id)))) 57 | (testing "edit! - Edit an existing card and returns the updated card" 58 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 59 | deck-id (decks/create! *conn* user-id new-deck) 60 | new-card {:card/deck [:deck/id deck-id] 61 | :card/front "What is Cloujure" 62 | :card/back "A programming language"} 63 | card-id (SUT/create! *conn* deck-id new-card) 64 | card-params {:card/back "A functional programming language"} 65 | edited-card (SUT/edit! *conn* deck-id card-id card-params)] 66 | (is (s/valid? ::SUT/card edited-card)))) 67 | (testing "delete! - Delete an existing card - returns deleted card" 68 | (let [new-deck (merge (gen/generate (s/gen ::decks/deck))) 69 | deck-id (decks/create! *conn* user-id new-deck) 70 | new-card {:card/deck [:deck/id deck-id] 71 | :card/front "What is Cloujure" 72 | :card/back "A programming language"} 73 | card-id (SUT/create! *conn* deck-id new-card) 74 | deleted-card (SUT/delete! *conn* deck-id card-id)] 75 | (is (s/valid? ::SUT/card deleted-card)) 76 | (is (nil? (SUT/fetch (d/db *conn*) deck-id card-id))))))) 77 | -------------------------------------------------------------------------------- /test/grok/db/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.core-test 2 | (:require [clojure.test :refer [is deftest testing use-fixtures]] 3 | [grok.db.with-db :refer [with-db *conn*]])) 4 | 5 | (use-fixtures :each with-db) 6 | 7 | (deftest conn 8 | (testing "create-conn" 9 | (is (not (nil? *conn*))))) 10 | -------------------------------------------------------------------------------- /test/grok/db/decks_test.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.decks-test 2 | (:require [clojure.test :refer [is deftest testing use-fixtures]] 3 | [datomic.api :as d] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test.check.generators :as gen] 6 | [grok.db.decks :as SUT] 7 | [grok.db.with-db :refer [with-db *conn*]])) 8 | 9 | (use-fixtures :each with-db) 10 | 11 | (deftest decks 12 | (let [user-id (:user/id (d/entity (d/db *conn*) [:user/email "test@test.com"]))] 13 | (testing "browse - returns empty vector, user has not created any deck" 14 | (let [decks (SUT/browse (d/db *conn*) user-id)] 15 | (is (= true (vector? decks))) 16 | (is (= true (empty? decks))))) 17 | 18 | (testing "browse - returns vector of decks, if available" 19 | (let [new-deck (merge (gen/generate (s/gen ::SUT/deck)) 20 | {:deck/author [:user/id user-id]})] 21 | @(d/transact *conn* [new-deck]) 22 | (let [decks (SUT/browse (d/db *conn*) user-id)] 23 | (is (= true (vector? decks))) 24 | (is (= false (empty? decks)))))) 25 | 26 | (testing "fetch - returns a single deck by deck ID, belonging to a user" 27 | (let [deck-id (d/squuid) 28 | new-deck (merge (gen/generate (s/gen ::SUT/deck)) 29 | {:deck/id deck-id 30 | :deck/author [:user/id user-id]})] 31 | @(d/transact *conn* [new-deck]) 32 | (let [deck (SUT/fetch (d/db *conn*) user-id deck-id)] 33 | (is (= true (map? deck))) 34 | (is (= false (empty? deck)))))) 35 | (testing "fetch - returns nil if not found" 36 | (let [deck-id (d/squuid) 37 | deck (SUT/fetch (d/db *conn*) user-id deck-id)] 38 | (is (= false (map? deck))) 39 | (is (= true (nil? deck))))) 40 | 41 | (testing "create! - create a new deck" 42 | (let [new-deck (gen/generate (s/gen ::SUT/deck)) 43 | deck-id (SUT/create! *conn* user-id new-deck)] 44 | (is (uuid? deck-id)))) 45 | (testing "edit! - edit an existing deck" 46 | (let [new-deck (gen/generate (s/gen ::SUT/deck)) 47 | deck-id (SUT/create! *conn* user-id new-deck) 48 | deck-params {:deck/title "Learning Datomic"} 49 | edited-deck (SUT/edit! *conn* user-id deck-id deck-params)] 50 | (is (= (:deck/title deck-params) (:deck/title edited-deck))))) 51 | (testing "delete! - delete an existing deck" 52 | (let [deck-id (SUT/create! *conn* user-id (gen/generate (s/gen ::SUT/deck))) 53 | deck (SUT/delete! *conn* user-id deck-id)] 54 | (is (= true (s/valid? ::SUT/deck deck))) 55 | (is (= nil (SUT/fetch (d/db *conn*) user-id deck-id))))))) 56 | -------------------------------------------------------------------------------- /test/grok/db/users_test.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.users-test 2 | (:require [clojure.test :refer [is deftest testing use-fixtures]] 3 | [datomic.api :as d] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test.check.generators :as gen] 6 | [grok.db.users :as SUT] 7 | [grok.db.with-db :refer [with-db *conn*]])) 8 | 9 | (use-fixtures :each with-db) 10 | 11 | (deftest user 12 | (testing "create!" 13 | (let [user-params (gen/generate (s/gen ::SUT/user)) 14 | uid (SUT/create! *conn* user-params)] 15 | (is (not (nil? uid))) 16 | (is (= true (uuid? uid))))) 17 | (testing "fetch" 18 | (let [uid (SUT/create! *conn* (gen/generate (s/gen ::SUT/user))) 19 | user (SUT/fetch (d/db *conn*) uid)] 20 | (is (= true (s/valid? ::SUT/user user))))) 21 | (testing "edit!" 22 | (let [uid (SUT/create! *conn* (gen/generate (s/gen ::SUT/user))) 23 | user (SUT/edit! *conn* uid {:user/username "jane.doe"})] 24 | (is (= true (s/valid? ::SUT/user user))) 25 | (is (= "jane.doe" (:user/username user))))) 26 | (testing "delete!" 27 | (let [uid (SUT/create! *conn* (gen/generate (s/gen ::SUT/user))) 28 | user (SUT/delete! *conn* uid)] 29 | (is (= true (s/valid? ::SUT/user user))) 30 | (is (= nil (SUT/fetch (d/db *conn*) uid)))))) 31 | -------------------------------------------------------------------------------- /test/grok/db/with_db.clj: -------------------------------------------------------------------------------- 1 | (ns grok.db.with-db 2 | (:require [grok.db.core :as SUT] 3 | [grok.db.users :as u] 4 | [grok.db.schema :refer [schema]] 5 | [clojure.spec.alpha :as s] 6 | [clojure.test.check.generators :as gen] 7 | [datomic.api :as d])) 8 | 9 | (def ^:dynamic *conn* nil) 10 | 11 | (def user-params) 12 | (def sample-user 13 | (-> (gen/generate (s/gen ::u/user)) 14 | (merge {:user/email "test@test.com" 15 | :user/id (d/squuid)}))) 16 | 17 | (defn fresh-db [] 18 | (let [db-uri (str "datomic:mem://" (gensym)) 19 | conn (SUT/create-conn db-uri)] 20 | 21 | (d/transact conn schema) 22 | (d/transact conn [sample-user]) 23 | conn)) 24 | 25 | (defn with-db [f] 26 | (binding [*conn* (fresh-db)] 27 | (f))) 28 | --------------------------------------------------------------------------------