├── src ├── clj │ └── hn_clj_pedestal_re_frame │ │ ├── core.clj │ │ ├── sql.clj │ │ ├── system.clj │ │ ├── db.clj │ │ ├── server.clj │ │ └── schema.clj └── cljs │ └── hn_clj_pedestal_re_frame │ ├── db.cljs │ ├── events │ ├── authentication.cljs │ ├── graph_ql │ │ ├── subscriptions.cljs │ │ ├── queries.cljs │ │ └── mutations.cljs │ ├── core.cljs │ ├── init.cljs │ └── utils.cljs │ ├── graph_ql │ ├── subscriptions.cljs │ ├── mutations.cljs │ └── queries.cljs │ ├── core.cljs │ ├── config.cljs │ ├── subs.cljs │ ├── routes.cljs │ └── views │ ├── core.cljs │ ├── utils.cljs │ ├── forms.cljs │ └── lists.cljs ├── resources ├── public │ ├── favicon.ico │ ├── manifest.json │ ├── index.html │ └── css │ │ └── index.css ├── sql │ └── queries.sql └── hn-schema.edn ├── .gitignore ├── bin ├── setup-db.sh └── setup-db.sql ├── dev-resources ├── hn-data.edn ├── hn_clj_pedestal_re_frame │ └── test_utils.clj ├── user.clj └── logback-test.xml ├── test └── hn_clj_pedestal_re_frame │ └── system_tests.clj ├── project.clj └── README.md /src/clj/hn_clj_pedestal_re_frame/core.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.core) 2 | -------------------------------------------------------------------------------- /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/promesante/hn-clj-pedestal-re-frame/HEAD/resources/public/favicon.ico -------------------------------------------------------------------------------- /src/clj/hn_clj_pedestal_re_frame/sql.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.sql 2 | (:require [yesql.core :as yesql])) 3 | 4 | (yesql/defqueries "sql/queries.sql") 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /target 3 | /*-init.clj 4 | /resources/public/js/compiled 5 | out 6 | *~ 7 | .lein-repl-history 8 | .lein-failures 9 | .nrepl-port 10 | .rebel_readline_history 11 | *.DS_Store 12 | /logs 13 | *.html 14 | -------------------------------------------------------------------------------- /bin/setup-db.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dropdb --if-exists hndb 4 | createdb hndb 5 | 6 | dropuser --if-exists hn_role 7 | psql hndb -a <<__END 8 | create user hn_role password 'lacinia'; 9 | __END 10 | 11 | psql -Uhn_role hndb -f setup-db.sql 12 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/db.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.db) 2 | 3 | (def default-db 4 | { 5 | ; :name "re-frame" 6 | :loading? false 7 | :error false 8 | :new-links [] 9 | :search-links [] 10 | :top-links [] 11 | :link {} 12 | :count 0 13 | ; :new? false 14 | }) 15 | -------------------------------------------------------------------------------- /resources/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /dev-resources/hn-data.edn: -------------------------------------------------------------------------------- 1 | { 2 | :links [ 3 | { 4 | :id "link-0" 5 | :description "REMOTE: Prisma turns your database into a GraphQL API" 6 | :url "https://www.prismagraphql.com" 7 | } 8 | { 9 | :id "link-1" 10 | :description "REMOTE: The best GraphQL client" 11 | :url "https://www.apollographql.com/docs/react/" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/clj/hn_clj_pedestal_re_frame/system.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.system 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [hn-clj-pedestal-re-frame.schema :as schema] 5 | [hn-clj-pedestal-re-frame.server :as server] 6 | [hn-clj-pedestal-re-frame.db :as db])) 7 | 8 | (defn new-system 9 | [] 10 | (merge (component/system-map) 11 | (server/new-server) 12 | (schema/new-schema-provider) 13 | (db/new-db))) 14 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/authentication.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.authentication 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [hn-clj-pedestal-re-frame.db :as db] 5 | [hn-clj-pedestal-re-frame.config :as config])) 6 | 7 | 8 | (re-frame/reg-event-fx 9 | :logout 10 | (fn [{:keys [db]} _] 11 | {:remove-local-store ["token"] 12 | :db (-> db 13 | (assoc :loading? false) 14 | (assoc-in 15 | config/token-header-path 16 | nil)) 17 | :dispatch [:navigate :home]})) 18 | -------------------------------------------------------------------------------- /dev-resources/hn_clj_pedestal_re_frame/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.test-utils 2 | (:require 3 | [clojure.walk :as walk]) 4 | (:import 5 | (clojure.lang IPersistentMap))) 6 | 7 | (defn simplify 8 | "Converts all ordered maps nested within the map into standard hash maps, and 9 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems." 10 | [m] 11 | (walk/postwalk 12 | (fn [node] 13 | (cond 14 | (instance? IPersistentMap node) 15 | (into {} node) 16 | 17 | (seq? node) 18 | (vec node) 19 | 20 | :else 21 | node)) 22 | m)) 23 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/graph_ql/subscriptions.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.graph-ql.subscriptions) 2 | 3 | (def new-link 4 | "{ 5 | newLink { 6 | id 7 | url 8 | description 9 | created_at 10 | posted_by { 11 | id 12 | name 13 | } 14 | } 15 | }") 16 | 17 | (def new-vote 18 | "{ 19 | newVote { 20 | id 21 | link { 22 | id 23 | url 24 | description 25 | created_at 26 | posted_by { 27 | id 28 | name 29 | } 30 | votes { 31 | id 32 | user { 33 | id 34 | } 35 | } 36 | } 37 | user { 38 | id 39 | } 40 | } 41 | }") 42 | 43 | -------------------------------------------------------------------------------- /resources/public/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Verdana, Geneva, sans-serif; 5 | } 6 | 7 | input { 8 | max-width: 500px; 9 | } 10 | 11 | .gray { 12 | color: #828282; 13 | } 14 | 15 | .orange { 16 | background-color: #ff6600; 17 | } 18 | 19 | .background-gray { 20 | background-color: rgb(246,246,239); 21 | } 22 | 23 | .f11 { 24 | font-size: 11px; 25 | } 26 | 27 | .w85 { 28 | width: 85%; 29 | } 30 | 31 | .button { 32 | font-family: monospace; 33 | font-size: 10pt; 34 | color: black; 35 | background-color: buttonface; 36 | text-align: center; 37 | padding: 2px 6px 3px; 38 | border-width: 2px; 39 | border-style: outset; 40 | border-color: buttonface; 41 | cursor: pointer; 42 | max-width: 250px; 43 | } -------------------------------------------------------------------------------- /dev-resources/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [com.walmartlabs.lacinia :as lacinia] 4 | [clojure.java.browse :refer [browse-url]] 5 | [hn-clj-pedestal-re-frame.system :as system] 6 | [hn-clj-pedestal-re-frame.db :as db] 7 | [com.stuartsierra.component :as component])) 8 | 9 | (defonce system (system/new-system)) 10 | 11 | ; (defonce db (db/new-db)) 12 | 13 | (defn q 14 | [query-string] 15 | (-> system 16 | :schema-provider 17 | :schema 18 | (lacinia/execute query-string nil nil))) 19 | 20 | (defonce db 21 | ; [] 22 | (-> system 23 | ; :schema-provider 24 | :db)) 25 | ; (lacinia/execute query-string nil nil))) 26 | 27 | (defn start 28 | [] 29 | (alter-var-root #'system component/start-system) 30 | (browse-url "http://localhost:8888/") 31 | :started) 32 | 33 | (defn stop 34 | [] 35 | (alter-var-root #'system component/stop-system) 36 | :stopped) 37 | 38 | -------------------------------------------------------------------------------- /src/clj/hn_clj_pedestal_re_frame/db.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.db 2 | (:require 3 | [com.stuartsierra.component :as component] 4 | [yesql.core :as yesql]) 5 | (:import (com.mchange.v2.c3p0 ComboPooledDataSource))) 6 | 7 | (defn ^:private pooled-data-source 8 | [host dbname user password port] 9 | {:datasource 10 | (doto (ComboPooledDataSource.) 11 | (.setDriverClass "org.postgresql.Driver" ) 12 | (.setJdbcUrl (str "jdbc:postgresql://" host ":" port "/" dbname)) 13 | (.setUser user) 14 | (.setPassword password))}) 15 | 16 | (defrecord HackerNewsDb [ds] 17 | 18 | component/Lifecycle 19 | 20 | (start [this] 21 | (assoc this 22 | :connection (pooled-data-source "localhost" "hndb" "hn_role" "lacinia" 5432))) 23 | 24 | (stop [this] 25 | (-> ds :datasource .close) 26 | (assoc this :connection nil))) 27 | 28 | (defn new-db 29 | [] 30 | {:db (map->HackerNewsDb {})}) 31 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/graph_ql/subscriptions.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.graph-ql.subscriptions 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [hn-clj-pedestal-re-frame.events.utils :as utils] 5 | [hn-clj-pedestal-re-frame.db :as db])) 6 | 7 | 8 | (re-frame/reg-event-db 9 | :on-new-link 10 | (fn [db [_ {:keys [data errors] :as payload}]] 11 | (let [link-new (:newLink data) 12 | links-prev (:new-links db) 13 | created? (utils/created? link-new links-prev)] 14 | (if created? 15 | db 16 | (let [links (utils/add-link link-new links-prev)] 17 | (-> db 18 | (assoc :loading? false) 19 | (assoc :new-links links))))))) 20 | 21 | (re-frame/reg-event-fx 22 | :on-new-vote 23 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 24 | (let [vote-new (:newVote data)] 25 | {:dispatch [:on-new-vote-db vote-new]}))) 26 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/graph_ql/mutations.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.graph-ql.mutations) 2 | 3 | (def post 4 | "post($url:String!, $description:String!) { 5 | post( 6 | url: $url, 7 | description: $description 8 | ) { 9 | id 10 | } 11 | }") 12 | 13 | (def vote 14 | "vote($link_id:ID!) { 15 | vote( 16 | link_id: $link_id 17 | ) { 18 | id 19 | link { 20 | id 21 | } 22 | user { 23 | id 24 | } 25 | } 26 | }") 27 | 28 | (def signup 29 | "signup($email:String!, $password:String!, $name:String!) { 30 | signup( 31 | email: $email, 32 | password: $password, 33 | name: $name 34 | ) { 35 | token 36 | } 37 | }") 38 | 39 | (def login 40 | "login($email:String!, $password:String!) { 41 | login( 42 | email: $email, 43 | password: $password 44 | ) { 45 | token 46 | } 47 | }") 48 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/graph_ql/queries.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.graph-ql.queries) 2 | 3 | (def feed 4 | "FeedQuery($first: Int, $skip: Int) { 5 | feed( 6 | first: $first, 7 | skip: $skip 8 | ) { 9 | count 10 | links { 11 | id 12 | created_at 13 | url 14 | description 15 | posted_by { 16 | id 17 | name 18 | } 19 | votes { 20 | id 21 | user { 22 | id 23 | } 24 | } 25 | } 26 | } 27 | }") 28 | 29 | (def search 30 | "FeedSearchQuery($filter: String!) { 31 | feed(filter: $filter) { 32 | links { 33 | id 34 | url 35 | description 36 | created_at 37 | posted_by { 38 | id 39 | name 40 | } 41 | votes { 42 | id 43 | user { 44 | id 45 | } 46 | } 47 | } 48 | } 49 | }") 50 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/core.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.core 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [hodgepodge.core :refer [local-storage get-item set-item remove-item]] 5 | [hn-clj-pedestal-re-frame.routes :as routes] 6 | [hn-clj-pedestal-re-frame.db :as db])) 7 | 8 | 9 | (re-frame/reg-event-db 10 | :set-active-panel 11 | (fn [db [_ active-panel]] 12 | (assoc db :active-panel active-panel))) 13 | 14 | (re-frame/reg-fx 15 | :set-history 16 | (fn [route] 17 | (routes/set-history! route))) 18 | 19 | (re-frame/reg-event-fx 20 | :navigate 21 | (fn [_ [_ route]] 22 | {:set-history route})) 23 | 24 | (re-frame/reg-fx 25 | :set-local-store 26 | (fn [[key value]] 27 | (set-item local-storage key value))) 28 | 29 | (re-frame/reg-fx 30 | :remove-local-store 31 | (fn [key] 32 | (remove-item local-storage key))) 33 | 34 | (re-frame/reg-cofx 35 | :local-store 36 | (fn [coeffects key] 37 | (get-item local-storage key "empty"))) 38 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/core.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.core 2 | (:require 3 | [reagent.core :as reagent] 4 | [re-frame.core :as re-frame] 5 | [hn-clj-pedestal-re-frame.events.init :as events] 6 | [hn-clj-pedestal-re-frame.events.authentication] 7 | [hn-clj-pedestal-re-frame.events.core] 8 | [hn-clj-pedestal-re-frame.events.init] 9 | [hn-clj-pedestal-re-frame.events.utils] 10 | [hn-clj-pedestal-re-frame.events.graph-ql.queries] 11 | [hn-clj-pedestal-re-frame.events.graph-ql.mutations] 12 | [hn-clj-pedestal-re-frame.events.graph-ql.subscriptions] 13 | [hn-clj-pedestal-re-frame.routes :as routes] 14 | [hn-clj-pedestal-re-frame.views.core :as views] 15 | [hn-clj-pedestal-re-frame.config :as config])) 16 | 17 | (defn dev-setup [] 18 | (when config/debug? 19 | (enable-console-print!) 20 | (println "dev mode"))) 21 | 22 | (defn mount-root [] 23 | (re-frame/clear-subscription-cache!) 24 | (reagent/render [views/main-panel] 25 | (.getElementById js/document "app"))) 26 | 27 | (defn ^:export init [] 28 | (re-frame/dispatch-sync [::events/init]) 29 | (routes/start-history!) 30 | (dev-setup) 31 | (mount-root)) 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/init.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.init 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [re-graph.core :as re-graph] 5 | [hn-clj-pedestal-re-frame.db :as db] 6 | [hn-clj-pedestal-re-frame.graph-ql.subscriptions :as subscriptions])) 7 | 8 | 9 | (re-frame/reg-event-fx 10 | ::init 11 | (fn [{:keys [db]} [_ _]] 12 | {:db db/default-db 13 | :dispatch [:init-re-graph]})) 14 | 15 | (re-frame/reg-event-fx 16 | :init-re-graph 17 | (fn [{:keys [db]} [_ _]] 18 | {:db (-> db 19 | (assoc :loading? true) 20 | (assoc :error false)) 21 | :dispatch-n (list 22 | [::re-graph/init 23 | {:http-parameters 24 | {:with-credentials? false 25 | :headers nil}}] 26 | [::re-graph/subscribe 27 | :subscribe-to-new-links 28 | subscriptions/new-link 29 | {} 30 | [:on-new-link]] 31 | [::re-graph/subscribe 32 | :subscribe-to-new-votes 33 | subscriptions/new-vote 34 | {} 35 | [:on-new-vote]])})) 36 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/config.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.config) 2 | 3 | 4 | ;----------------------------------------------------------------------- 5 | ; Links per page 6 | ;----------------------------------------------------------------------- 7 | 8 | (def new-links-per-page 5) 9 | (def top-links-per-page 100) 10 | 11 | 12 | ;----------------------------------------------------------------------- 13 | ; Dates 14 | ;----------------------------------------------------------------------- 15 | 16 | (def date-messages [ 17 | {:conv 60 :message "less than 1 min ago"} 18 | {:conv 60 :message " min ago"} 19 | {:conv 24 :message " h ago"} 20 | {:conv 30 :message " days ago"} 21 | {:conv 365 :message " mo ago"} 22 | {:conv 10 :message " years ago"}]) 23 | 24 | (def mills-per-sec 1000) 25 | 26 | 27 | ;----------------------------------------------------------------------- 28 | ; Authentication 29 | ;----------------------------------------------------------------------- 30 | 31 | (def token-header-path 32 | [:re-graph :re-graph.internals/default :http-parameters :headers]) 33 | 34 | ;----------------------------------------------------------------------- 35 | ; Misc 36 | ;----------------------------------------------------------------------- 37 | 38 | (def debug? 39 | ^boolean goog.DEBUG) 40 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.subs 2 | (:require-macros [reagent.ratom :refer [reaction]]) 3 | (:require 4 | [re-frame.core :as re-frame] 5 | [hn-clj-pedestal-re-frame.config :as config])) 6 | 7 | 8 | ;----------------------------------------------------------------------- 9 | ; Core 10 | ;----------------------------------------------------------------------- 11 | 12 | (re-frame/reg-sub 13 | :active-panel 14 | (fn [db _] 15 | (:active-panel db))) 16 | 17 | (re-frame/reg-sub 18 | :loading? 19 | (fn [db] 20 | (:loading? db))) 21 | 22 | (re-frame/reg-sub 23 | :error? 24 | (fn [db] 25 | (:error db))) 26 | 27 | (re-frame/reg-sub 28 | :headers 29 | (fn [db _] 30 | (get-in db config/token-header-path))) 31 | 32 | (re-frame/reg-sub 33 | :auth? 34 | (fn [_] 35 | (re-frame/subscribe [:headers])) 36 | (fn [headers] 37 | (not (nil? headers)))) 38 | 39 | 40 | ;----------------------------------------------------------------------- 41 | ; Lists 42 | ;----------------------------------------------------------------------- 43 | 44 | (re-frame/reg-sub 45 | :new-links 46 | (fn [db] 47 | (:new-links db))) 48 | 49 | (re-frame/reg-sub 50 | :count 51 | (fn [db] 52 | (:count db))) 53 | 54 | (re-frame/reg-sub 55 | :top-links 56 | (fn [db] 57 | (:top-links db))) 58 | 59 | (re-frame/reg-sub 60 | :search-links 61 | (fn [db] 62 | (:search-links db))) 63 | -------------------------------------------------------------------------------- /bin/setup-db.sql: -------------------------------------------------------------------------------- 1 | drop table if exists link; 2 | drop table if exists usr; 3 | drop table if exists vote; 4 | 5 | create table usr ( 6 | id int generated by default as identity primary key, 7 | name text not null, 8 | email text not null, 9 | password text not null, 10 | created_at timestamp not null default current_timestamp, 11 | updated_at timestamp not null default current_timestamp); 12 | 13 | insert into usr (name, email, password) values 14 | ('John', 'john@hotmail.com', 'john'), 15 | ('Paul', 'paul@gmail.com', 'paul'); 16 | 17 | create table link ( 18 | id int generated by default as identity primary key, 19 | description text, 20 | url text not null, 21 | usr_id int not null, 22 | created_at timestamp not null default current_timestamp, 23 | updated_at timestamp not null default current_timestamp); 24 | 25 | insert into link (description, url, usr_id) values 26 | ('INIT - Prisma turns your database into a GraphQL API', 'https://www.prismagraphql.com', 1), 27 | ('INIT - The best GraphQL client', 'https://www.apollographql.com/docs/react/', 2); 28 | 29 | create table vote ( 30 | id int generated by default as identity primary key, 31 | link_id int not null, 32 | usr_id int not null, 33 | created_at timestamp not null default current_timestamp, 34 | updated_at timestamp not null default current_timestamp); 35 | 36 | insert into vote (link_id, usr_id) values 37 | (1, 1), 38 | (1, 2), 39 | (2, 1); 40 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.utils 2 | (:require 3 | [hn-clj-pedestal-re-frame.config :as config])) 4 | 5 | 6 | (defn created? [link links] 7 | (let [link-id (:id link) 8 | created? (some #(= link-id (:id %)) links)] 9 | created?)) 10 | 11 | (defn handle-drop [links] 12 | (if (> (count links) config/new-links-per-page) 13 | (drop-last links) 14 | links)) 15 | 16 | (defn add-link [link links] 17 | (let [link-vector [link] 18 | links-before-drop (concat link-vector links) 19 | links-after-drop (handle-drop links-before-drop)] 20 | links-after-drop)) 21 | 22 | (defn voted? [vote links] 23 | (let [link-id (get-in vote [:link :id]) 24 | usr (:user vote) 25 | usr-id (:id usr) 26 | link (first (filter (fn [link] (= link-id (:id link))) links)) 27 | votes (:votes link) 28 | voted? (some #(= usr-id (get-in % [:user :id])) votes)] 29 | voted?)) 30 | 31 | (defn add-vote [vote links] 32 | (let [link-id (get-in vote [:link :id]) 33 | usr (:user vote) 34 | vote-id (:id vote) 35 | new-vote {:id vote-id :user usr} 36 | links-updated (map 37 | (fn [link] 38 | (if (= link-id (:id link)) 39 | (update link :votes conj new-vote) 40 | link)) 41 | links)] 42 | links-updated)) 43 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/routes.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.routes 2 | (:require [bidi.bidi :as bidi] 3 | [pushy.core :as pushy] 4 | [re-frame.core :as re-frame])) 5 | 6 | (def routes ["/" 7 | {"" :home 8 | "new/" {[:page] :new} 9 | "top" :top 10 | "search" :search 11 | "create" :create 12 | "login" :login 13 | "logout" :logout 14 | true :not-found}]) 15 | 16 | (def panel-sufix "-panel") 17 | (def events {:home #(re-frame/dispatch [:fetch-new-links 1]) 18 | :new #(re-frame/dispatch [:fetch-new-links (:page %)]) 19 | :top #(re-frame/dispatch [:fetch-top-links]) 20 | :logout #(re-frame/dispatch [:logout])}) 21 | 22 | (defn switch-panel [panel-id] 23 | (let [panel-name (keyword (str (name panel-id) panel-sufix))] 24 | (re-frame/dispatch [:set-active-panel panel-name]))) 25 | 26 | (defn handle-match [match] 27 | (let [{:keys [handler route-params]} match 28 | event (get events handler)] 29 | (if (nil? event) 30 | (switch-panel handler) 31 | (event route-params)))) 32 | 33 | (defn bidi-matcher 34 | "Will match a URL to a route" 35 | [s] 36 | (let [match (bidi/match-route routes s)] 37 | match)) 38 | 39 | (def history 40 | (pushy/pushy handle-match bidi-matcher)) 41 | 42 | (defn start-history! [] 43 | (pushy/start! history)) 44 | 45 | (def url-for (partial bidi/path-for routes)) 46 | 47 | (defn set-history! [route] 48 | (pushy/set-token! history (url-for route))) 49 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/views/core.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.views.core 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [hn-clj-pedestal-re-frame.views.lists :as lists] 5 | [hn-clj-pedestal-re-frame.views.forms :as forms] 6 | [hn-clj-pedestal-re-frame.routes :as routes] 7 | [hn-clj-pedestal-re-frame.subs :as subs])) 8 | 9 | 10 | (defn- panels [panel-name] 11 | (case panel-name 12 | :new-panel [lists/new-panel] 13 | :top-panel [lists/top-panel] 14 | :search-panel [lists/search-panel] 15 | :create-panel [forms/create-panel] 16 | :login-panel [forms/login-panel] 17 | [:div])) 18 | 19 | (defn show-panel [panel-name] 20 | [panels panel-name]) 21 | 22 | (defn header-panel [] 23 | (let [loading? (re-frame/subscribe [:loading?]) 24 | auth? (re-frame/subscribe [:auth?]) 25 | route (if @auth? "logout" "login")] 26 | [:div.flex.pa1.justify-between.nowrap.orange 27 | [:div.flex.flex-fixed.black 28 | [:div.fw7.mr1 "Hacker News"] 29 | [:a.ml1.no-underline.black {:href (routes/url-for :new :page 1)} "new"] 30 | [:div.ml1 "|"] 31 | [:a.ml1.no-underline.black {:href "/top"} "top"] 32 | [:div.ml1 "|"] 33 | [:a.ml1.no-underline.black {:href (routes/url-for :search)} "search"] 34 | [:div.ml1 "|"] 35 | [:a.ml1.no-underline.black {:href (routes/url-for :create)} "submit"]] 36 | [:div.flex.flex-fixed 37 | [:a.ml1.no-underline.black {:href (str "/" route)} route]]])) 38 | 39 | (defn main-panel [] 40 | (let [active-panel (re-frame/subscribe [:active-panel])] 41 | [:div.ph3.pv1.background-gray 42 | [header-panel] 43 | [show-panel @active-panel]])) 44 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/views/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.views.utils 2 | (:require 3 | [cljs-time.core :as time] 4 | [cljs-time.format :as format] 5 | [cljs-time.coerce :as coerce])) 6 | 7 | (def date-messages [ 8 | {:conv 60 :message "less than 1 min ago"} 9 | {:conv 60 :message " min ago"} 10 | {:conv 24 :message " h ago"} 11 | {:conv 30 :message " days ago"} 12 | {:conv 365 :message " mo ago"} 13 | {:conv 10 :message " years ago"}]) 14 | 15 | (def mills-per-sec 1000) 16 | 17 | (defn render-date-message 18 | [elapsed mills-prev messages] 19 | (let [current (first messages) 20 | pending (rest messages) 21 | message (:message current) 22 | conv (:conv current) 23 | mills-curr (* conv mills-prev)] 24 | (if (< elapsed mills-curr) 25 | (let [value (quot elapsed mills-prev)] 26 | (str value message)) 27 | (if (nil? pending) 28 | (let [value (quot elapsed mills-curr)] 29 | (str value message)) 30 | (render-date-message elapsed mills-curr pending))))) 31 | 32 | (defn time-difference 33 | [current previous] 34 | (let [elapsed (- current previous)] 35 | (render-date-message elapsed mills-per-sec date-messages))) 36 | 37 | (defn parse-date 38 | [date] 39 | (let [formatter (format/formatter "yyyy-MM-dd HH:mm:ss.SSS") 40 | sliced (subs date 0 (- (count date) 3)) 41 | formatted (format/parse formatter sliced)] 42 | formatted)) 43 | 44 | (defn time-diff-for-date 45 | [date] 46 | (let [now (coerce/to-long (time/now)) 47 | formatted (parse-date date) 48 | updated (coerce/to-long formatted)] 49 | (time-difference now updated))) 50 | -------------------------------------------------------------------------------- /src/clj/hn_clj_pedestal_re_frame/server.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.server 2 | (:require [com.stuartsierra.component :as component] 3 | [clojure.java.io :as io] 4 | [com.walmartlabs.lacinia.pedestal :as lp] 5 | [io.pedestal.http :as http])) 6 | 7 | (defn html-response 8 | [html] 9 | {:status 200 :body html :headers {"Content-Type" "text/html"}}) 10 | 11 | ;; Gather some data from the user to retain in their session. 12 | (defn index-page 13 | "Prompt a user for their name, then remember it." 14 | [req] 15 | (html-response 16 | (slurp (io/resource "public/index.html")))) 17 | 18 | (def root-route 19 | ["/" :get `index-page]) 20 | 21 | (defn add-route 22 | [service-map] 23 | (let [{routes ::http/routes} service-map 24 | ext-routes (conj routes root-route)] 25 | (assoc service-map ::http/routes ext-routes))) 26 | 27 | (defrecord Server [schema-provider server port] 28 | 29 | component/Lifecycle 30 | (start [this] 31 | (assoc this :server (-> schema-provider 32 | :schema 33 | (lp/service-map 34 | {:graphiql true 35 | :ide-path "/graphiql" 36 | :port port 37 | :subscriptions true 38 | :ide-headers {:authorization "Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyLWlkIjozfQ.JH0Q2flkonyDPk_yiSrTK5VSKrbrsdR0FEePMgiEwDE"} 39 | }) 40 | (merge {::http/resource-path "/public"}) 41 | (add-route) 42 | http/create-server 43 | http/start))) 44 | 45 | (stop [this] 46 | (http/stop server) 47 | (assoc this :server nil))) 48 | 49 | (defn new-server 50 | [] 51 | {:server (component/using (map->Server {:port 8888}) 52 | [:schema-provider])}) 53 | -------------------------------------------------------------------------------- /resources/sql/queries.sql: -------------------------------------------------------------------------------- 1 | 2 | -- name: list-links 3 | select id, description, url, usr_id, created_at, updated_at 4 | from link 5 | 6 | -- name: filter-links 7 | select id, description, url, usr_id, created_at, updated_at 8 | from link 9 | where description like :filter 10 | or url like :filter 11 | order by created_at DESC 12 | limit ?::integer 13 | offset ?::integer 14 | 15 | -- name: filter-links-count 16 | select count(*) 17 | from link 18 | where description like :filter 19 | or url like :filter 20 | 21 | -- name: filter-links-order 22 | select id, description, url, usr_id, created_at, updated_at 23 | from link 24 | where description like :filter 25 | or url like :filter 26 | limit ?::integer 27 | offset ?::integer 28 | order by :field :criteria 29 | 30 | -- name: find-link-by-id 31 | select id, description, url, usr_id, created_at, updated_at 32 | from link 33 | where id = ?::integer 34 | 35 | -- name: find-user-by-id 36 | select id, name, email, password, created_at, updated_at 37 | from usr 38 | where id = ?::integer 39 | 40 | -- name: find-user-by-email 41 | select id, name, email, password, created_at, updated_at 42 | from usr 43 | where email = :email 44 | 45 | -- name: find-votes-by-link-usr 46 | select id 47 | from vote 48 | where link_id = ?::integer 49 | and usr_id = ?::integer 50 | 51 | -- name: find-user-by-link 52 | select u.id, u.name, u.email, u.password, u.created_at, u.updated_at 53 | from link l 54 | inner join usr u 55 | on (l.usr_id = u.id) 56 | where l.id = ?::integer 57 | 58 | -- name: find-links-by-user 59 | select l.id, l.description, l.url, l.usr_id, l.created_at, l.updated_at 60 | from link l 61 | inner join usr u 62 | on (l.usr_id = u.id) 63 | where u.id = ?::integer 64 | 65 | -- name: find-votes-by-link 66 | select 67 | v.id, 68 | u.id as usr_id, u.name, u.email, u.password, 69 | u.created_at, u.updated_at 70 | from vote v 71 | inner join link l on (v.link_id = l.id) 72 | inner join usr u on (v.usr_id = u.id) 73 | where l.id = ?::integer 74 | 75 | -- name: insert-link 2 | 3 | 4 | 5 | 6 | 7 | %d{HH:mm:ss.SSS} %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | logs/hn-schema-log-${byDay}.txt 13 | true 14 | 15 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n 16 | 17 | 18 | 19 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/hn_clj_pedestal_re_frame/system_tests.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.system-tests 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [hn-clj-pedestal-re-frame.system :as system] 5 | [hn-clj-pedestal-re-frame.test-utils :refer [simplify]] 6 | [com.stuartsierra.component :as component] 7 | [com.walmartlabs.lacinia :as lacinia])) 8 | 9 | (defn ^:private test-system 10 | "Creates a new system suitable for testing, and ensures that 11 | the HTTP port won't conflict with a default running system." 12 | [] 13 | (-> (system/new-system) 14 | (assoc-in [:server :port] 8989))) 15 | 16 | (defn ^:private q 17 | "Extracts the compiled schema and executes a query." 18 | [system query variables] 19 | (-> system 20 | (get-in [:schema-provider :schema]) 21 | (lacinia/execute query variables nil) 22 | simplify)) 23 | 24 | (deftest can-write-link 25 | (let [system (component/start-system (test-system)) 26 | results (q system 27 | "mutation { post(url: \"https://macwright.org/2017/08/09/decentralize-ipfs.html\", description: \"So you want to decentralize your website with IPFS\") { id }}" 28 | nil)] 29 | (is (= {:data 30 | {:feed [{:id "3", 31 | :url "https://macwright.org/2017/08/09/decentralize-ipfs.html", 32 | :description "So you want to decentralize your website with IPFS"}]}} 33 | results)) 34 | (component/stop-system system))) 35 | 36 | (deftest can-read-links-list 37 | (let [system (component/start-system (test-system)) 38 | results (q system 39 | "{ feed { id url description }}" 40 | nil)] 41 | (is (= {:data 42 | {:feed [{:id "1", 43 | :url "https://www.prismagraphql.com", 44 | :description "INIT - Prisma turns your database into a GraphQL API"} 45 | {:id "2", 46 | :url "https://www.apollographql.com/docs/react/", 47 | :description "INIT - The best GraphQL client"}]}} 48 | results)) 49 | (component/stop-system system))) 50 | 51 | (deftest can-write-user 52 | (let [system (component/start-system (test-system)) 53 | results (q system 54 | "mutation { signup( email: \"lmrosso@hotmail.com\", password: \"password\", name: \"Luis\" ) { id }}" 55 | nil)] 56 | (is (= {:data 57 | {:feed [{:id "3", 58 | :url "https://macwright.org/2017/08/09/decentralize-ipfs.html", 59 | :description "So you want to decentralize your website with IPFS"}]}} 60 | results)) 61 | (component/stop-system system))) 62 | 63 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject hn-clj-pedestal-re-frame "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.9.0"] 3 | [org.clojure/clojurescript "1.10.238"] 4 | [com.stuartsierra/component "0.3.2"] 5 | [com.walmartlabs/lacinia-pedestal "0.10.0"] 6 | ; [com.walmartlabs/lacinia-pedestal "0.10.0-SNAPSHOT"] 7 | [org.postgresql/postgresql "42.2.5.jre7"] 8 | [com.mchange/c3p0 "0.9.5.2"] 9 | [io.aviso/logging "0.3.1"] 10 | [buddy/buddy-sign "2.1.0"] 11 | [buddy/buddy-hashers "1.3.0"] 12 | [com.andrewmcveigh/cljs-time "0.5.2"] 13 | [kibu/pushy "0.3.8"] 14 | [bidi "2.1.2"] 15 | [reagi "0.10.1"] 16 | [yesql "0.5.3"] 17 | [hodgepodge "0.1.3"] 18 | [reagent "0.7.0"] 19 | [re-frame "0.10.5"] 20 | [re-graph "0.1.8-HN"] 21 | ; [re-graph "0.1.9-SNAPSHOT"] 22 | ] 23 | 24 | :plugins [[lein-cljsbuild "1.1.7"]] 25 | 26 | :min-lein-version "2.5.3" 27 | 28 | :source-paths ["src/clj" "src/cljs"] 29 | 30 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 31 | 32 | :figwheel {:css-dirs ["resources/public/css"]} 33 | 34 | :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]} 35 | 36 | :profiles 37 | {:dev 38 | {:dependencies [[binaryage/devtools "0.9.10"] 39 | [day8.re-frame/re-frame-10x "0.3.3"] 40 | [day8.re-frame/tracing "0.5.1"] 41 | [figwheel-sidecar "0.5.16"] 42 | [cider/cider-nrepl "0.16.0"] 43 | [cider/piggieback "0.4.0"] 44 | [re-frisk "0.5.3"]] 45 | 46 | :plugins [[lein-figwheel "0.5.16"]]} 47 | :prod { :dependencies [[day8.re-frame/tracing-stubs "0.5.1"]]} 48 | } 49 | 50 | :cljsbuild 51 | {:builds 52 | [{:id "dev" 53 | :source-paths ["src/cljs"] 54 | :figwheel {:on-jsload "hn-clj-pedestal-re-frame.core/mount-root"} 55 | :compiler {:main hn-clj-pedestal-re-frame.core 56 | :output-to "resources/public/js/compiled/app.js" 57 | :output-dir "resources/public/js/compiled/out" 58 | :asset-path "js/compiled/out" 59 | :source-map-timestamp true 60 | :preloads [devtools.preload 61 | day8.re-frame-10x.preload 62 | re-frisk.preload] 63 | :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true 64 | "day8.re_frame.tracing.trace_enabled_QMARK_" true} 65 | :external-config {:devtools/config {:features-to-install :all}} 66 | }} 67 | 68 | {:id "min" 69 | :source-paths ["src/cljs"] 70 | :compiler {:main hn-clj-pedestal-re-frame.core 71 | :output-to "resources/public/js/compiled/app.js" 72 | :optimizations :advanced 73 | :closure-defines {goog.DEBUG false} 74 | :pretty-print false}} 75 | 76 | 77 | ]} 78 | ) 79 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/graph_ql/queries.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.graph-ql.queries 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [re-graph.core :as re-graph] 5 | [hn-clj-pedestal-re-frame.db :as db] 6 | [hn-clj-pedestal-re-frame.config :as config] 7 | [hn-clj-pedestal-re-frame.graph-ql.queries :as queries])) 8 | 9 | 10 | ;------------------------------------------------------------------------- 11 | ; New Links 12 | ;------------------------------------------------------------------------- 13 | 14 | (re-frame/reg-event-fx 15 | :fetch-new-links 16 | (fn [{:keys [db]} [_ page]] 17 | (let [first config/new-links-per-page 18 | skip (* config/new-links-per-page (- page 1))] 19 | {:db (-> db 20 | (assoc :loading? true) 21 | (assoc :error false)) 22 | :dispatch [::re-graph/query 23 | queries/feed 24 | {:first first :skip skip} 25 | [:on-feed-new]]}))) 26 | 27 | (re-frame/reg-event-fx 28 | :on-feed-new 29 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 30 | (let [links (get-in data [:feed :links]) 31 | count (get-in data [:feed :count])] 32 | {:db (-> db 33 | (assoc :loading? false) 34 | (assoc :new-links links) 35 | (assoc :count count)) 36 | :dispatch [:set-active-panel :new-panel]}))) 37 | 38 | 39 | ;------------------------------------------------------------------------- 40 | ; Top Links 41 | ;------------------------------------------------------------------------- 42 | 43 | (re-frame/reg-event-fx 44 | :fetch-top-links 45 | (fn [{:keys [db]} [_ _]] 46 | (let [first config/top-links-per-page 47 | skip 1] 48 | {:db (-> db 49 | (assoc :loading? true) 50 | (assoc :error false)) 51 | :dispatch [::re-graph/query 52 | queries/feed 53 | {:first first :skip skip} 54 | [:on-feed-top]]}))) 55 | 56 | (re-frame/reg-event-fx 57 | :on-feed-top 58 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 59 | (let [links (get-in data [:feed :links]) 60 | votes (map (fn [link] (count (:votes link))) links) 61 | links-with-votes (filter (fn [link] (not (empty? (:votes link)))) links) 62 | ranked-links (sort 63 | #(compare (count (:votes %2)) (count (:votes %1))) 64 | links-with-votes)] 65 | {:db (-> db 66 | (assoc :loading? false) 67 | (assoc :top-links ranked-links)) 68 | :dispatch [:set-active-panel :top-panel]}))) 69 | 70 | 71 | ;------------------------------------------------------------------------- 72 | ; Search Links 73 | ;------------------------------------------------------------------------- 74 | 75 | (re-frame/reg-event-fx 76 | :search-links 77 | (fn [{:keys [db]} [_ filter]] 78 | {:db (-> db 79 | (assoc :loading? true) 80 | (assoc :error false)) 81 | :dispatch [::re-graph/query 82 | queries/search 83 | {:filter filter} 84 | [:on-search-links]]})) 85 | 86 | (re-frame/reg-event-db 87 | :on-search-links 88 | (fn [db [_ {:keys [data errors] :as payload}]] 89 | (let [links (get-in data [:feed :links])] 90 | (-> db 91 | (assoc :loading? false) 92 | (assoc :search-links links))))) 93 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/views/forms.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.views.forms 2 | (:require 3 | [reagent.core :as reagent] 4 | [re-frame.core :as re-frame])) 5 | 6 | 7 | (defn create-panel [] 8 | (let [loading? (re-frame/subscribe [:loading?]) 9 | error? (re-frame/subscribe [:error?]) 10 | description (reagent/atom "") 11 | url (reagent/atom "") 12 | on-click (fn [_] 13 | (when-not (or (empty? @description) (empty? @url)) 14 | (re-frame/dispatch [:create-link @description @url]) 15 | (reset! description "") 16 | (reset! url "")))] 17 | (fn [] 18 | [:div 19 | [:div.flex.flex-column.mt3 20 | [:input.mb2 {:type "text" 21 | :placeholder "A description for the link" 22 | :on-change #(reset! description (-> % .-target .-value))}] 23 | [:input.mb2 {:type "text" 24 | :placeholder "The URL for the link" 25 | :on-change #(reset! url (-> % .-target .-value))}] 26 | [:span.input-group-btn 27 | [:button.btn.btn-default {:type "button" 28 | :on-click #(when-not @loading? (on-click %))} 29 | "Go"] 30 | ]] 31 | (when @error? 32 | [:p.error-text.text-danger "Error in link creation"])]))) 33 | 34 | (defn login-panel [] 35 | (let [loading? (re-frame/subscribe [:loading?]) 36 | error? (re-frame/subscribe [:error?]) 37 | name (reagent/atom "") 38 | email (reagent/atom "") 39 | password (reagent/atom "") 40 | login (reagent/atom true) 41 | on-click-signup (fn [_] 42 | (when-not (or (empty? @name) (empty? @email) (empty? @password)) 43 | (re-frame/dispatch [:signup @name @email @password]) 44 | (reset! name "") 45 | (reset! email "") 46 | (reset! password ""))) 47 | on-click-login (fn [_] 48 | (when-not (or (empty? @email) (empty? @password)) 49 | (re-frame/dispatch [:login @email @password]) 50 | (reset! email "") 51 | (reset! password "")))] 52 | (fn [] 53 | [:div 54 | [:h4 {:class "mv3"} (if @login "Login" "Sign Up")] 55 | [:div.flex.flex-column.mt3 56 | (when (not @login) 57 | [:input.mb2 {:type "text" 58 | :placeholder "Your name" 59 | :on-change #(reset! name (-> % .-target .-value))}]) 60 | [:input.mb2 {:type "text" 61 | :placeholder "Your email address" 62 | :on-change #(reset! email (-> % .-target .-value))}] 63 | [:input.mb2 {:type "password" 64 | :placeholder "Choose a safe password" 65 | :on-change #(reset! password (-> % .-target .-value))}] 66 | [:span.input-group-btn 67 | (if @login 68 | [:button.btn.btn-default {:type "button" 69 | :on-click #(when-not @loading? 70 | (on-click-login %))} 71 | "login"] 72 | [:button.btn.btn-default {:type "button" 73 | :on-click #(when-not @loading? 74 | (on-click-signup %))} 75 | "create account"]) 76 | [:button.btn.btn-default {:type "button" 77 | :on-click #(swap! login not)} 78 | (if @login 79 | "need to create an account?" 80 | "already have an account?")]]] 81 | (when @error? 82 | [:p.error-text.text-danger "Error in login / signup"])]))) 83 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/views/lists.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.views.lists 2 | (:require 3 | [clojure.string :as str] 4 | [reagent.core :as reagent] 5 | [re-frame.core :as re-frame] 6 | [hn-clj-pedestal-re-frame.routes :as routes] 7 | [hn-clj-pedestal-re-frame.subs :as subs] 8 | [hn-clj-pedestal-re-frame.config :as config] 9 | [hn-clj-pedestal-re-frame.views.utils :as utils])) 10 | 11 | 12 | ;----------------------------------------------------------------------- 13 | ; Utils 14 | ;----------------------------------------------------------------------- 15 | 16 | (defn parse-page [] 17 | (let [path-name js/window.location.pathname 18 | path-elems (str/split path-name #"/") 19 | path-length (count path-elems)] 20 | (if (and (= path-length 3) (= "new" (get path-elems 1))) 21 | (js/parseInt (get path-elems 2)) 22 | 1))) 23 | 24 | (defn parse-path [] 25 | (let [path-name js/window.location.pathname 26 | path-elems (str/split path-name #"/") 27 | path (get path-elems 1)] 28 | path)) 29 | 30 | 31 | ;----------------------------------------------------------------------- 32 | ; Record 33 | ;----------------------------------------------------------------------- 34 | 35 | (defn link-record [] 36 | (fn [idx link] 37 | (let [{:keys [id created_at description url posted_by votes]} link 38 | link-id (js/parseInt id) 39 | auth? (re-frame/subscribe [:auth?]) 40 | time-diff (utils/time-diff-for-date created_at) 41 | path (parse-path) 42 | new? (or (nil? path) (= "new" path))] 43 | [:div.flex.mt2.items-start 44 | [:div.flex.items-center 45 | [:span.gray (str (inc idx) ".")] 46 | (when (and @auth? new?) 47 | [:div.f6.lh-copy.gray 48 | {:on-click #(re-frame/dispatch [:vote-link link-id])} "▲"]) 49 | [:div.ml1 50 | [:div (str description " (" url ")")] 51 | [:div.f6.lh-copy.gray 52 | (str (count votes) " votes | by " (:name posted_by) " " time-diff)]]]]))) 53 | 54 | 55 | ;----------------------------------------------------------------------- 56 | ; Lists 57 | ;----------------------------------------------------------------------- 58 | 59 | (defn new-panel [] 60 | (let [links (re-frame/subscribe [:new-links]) 61 | count (re-frame/subscribe [:count]) 62 | auth? (re-frame/subscribe [:auth?]) 63 | page (parse-page) 64 | last (quot @count config/new-links-per-page)] 65 | [:div 66 | (when @auth? [:div ""]) 67 | (map-indexed (link-record) @links) 68 | [:div.flex.ml4.mv3.gray 69 | (when (> page 1) 70 | [:a.ml1.no-underline.gray 71 | {:href (routes/url-for :new :page (- page 1))} "Previous"]) 72 | (when (< page last) 73 | [:a.ml1.no-underline.gray 74 | {:href (routes/url-for :new :page (+ page 1))} "Next"])]])) 75 | 76 | (defn top-panel [] 77 | (let [links (re-frame/subscribe [:top-links])] 78 | [:div 79 | (map-indexed (link-record) @links)])) 80 | 81 | (defn search-panel [] 82 | (let [loading? (re-frame/subscribe [:loading?]) 83 | error? (re-frame/subscribe [:error?]) 84 | links (re-frame/subscribe [:search-links]) 85 | filter (reagent/atom "") 86 | on-click (fn [_] 87 | (when-not (or (empty? @filter)) 88 | (re-frame/dispatch [:search-links @filter]) 89 | (reset! filter "")))] 90 | (fn [] 91 | [:div 92 | [:div "Search" 93 | [:input {:type "text" 94 | :on-change #(reset! filter (-> % .-target .-value))}] 95 | [:button {:type "button" 96 | :on-click #(when-not @loading? (on-click %))} 97 | "OK"]] 98 | (map-indexed (link-record) @links) 99 | (when @error? 100 | [:p.error-text.text-danger "Error in search"])]))) 101 | -------------------------------------------------------------------------------- /resources/hn-schema.edn: -------------------------------------------------------------------------------- 1 | {:objects 2 | {:Link 3 | {:description "A link posted to Hacker News" 4 | :fields 5 | {:id {:type (non-null ID)} 6 | :created_at {:type (non-null String) 7 | :description "Link creation datetime"} 8 | :description {:type (non-null String) 9 | :description "Link description"} 10 | :url {:type (non-null String) 11 | :description "Link url"} 12 | :posted_by {:type :User 13 | :description "User who posted this link" 14 | :resolve :Link/user} 15 | :votes {:type (list :Vote) 16 | :description "Link's votes" 17 | :resolve :Link/votes} 18 | } 19 | } 20 | 21 | :User 22 | {:description "User posting links to Hacker News" 23 | :fields 24 | {:id {:type (non-null ID)} 25 | :name {:type (non-null String) 26 | :description "User's name"} 27 | :email {:type (non-null String) 28 | :description "User's email"} 29 | :password {:type (non-null String) 30 | :description "User's password"} 31 | :links {:type (list :Link) 32 | :description "User's published links" 33 | :resolve :User/links} 34 | } 35 | } 36 | 37 | :AuthPayload 38 | {:description "Data returned by Login and Signup" 39 | :fields 40 | {:token {:type String 41 | :description "User's token"} 42 | :user {:type :User 43 | :description "User's data"} 44 | }} 45 | 46 | :Vote 47 | {:description "A vote for a link submitted to Hacker News" 48 | :fields 49 | {:id {:type (non-null ID)} 50 | :link {:type :Link 51 | :description "Voted link"} 52 | :user {:type :User 53 | :description "Voting user"} 54 | } 55 | } 56 | 57 | :Feed 58 | {:description "Object holding returned filtered, paginated linkes, and their count" 59 | :fields 60 | {:links {:type (non-null (list (non-null :Link))) 61 | :description "Filtered, paginated linkes"} 62 | :count {:type (non-null Int)}} 63 | } 64 | 65 | } 66 | 67 | :enums 68 | {:LinkOrderByInput 69 | {:description "Combination of field and criteria for Link sorting" 70 | :values 71 | [:DESCRIPTION_ASC :DESCRIPTION_DESC 72 | :URL_ASC :URL_DESC 73 | :CREATED_AT_ASC :CREATED_AT_DESC] 74 | } 75 | } 76 | 77 | :queries 78 | {:feed 79 | {:type (non-null Feed) 80 | :args 81 | { 82 | :filter {:type String} 83 | :skip {:type Int} 84 | :first {:type Int} 85 | ; :order_by {:type :LinkOrderByInput} 86 | } 87 | :resolve :query/feed}} 88 | 89 | :mutations 90 | {:post 91 | {:type (non-null :Link) 92 | :description "Post a new Link" 93 | :args 94 | { 95 | :url {:type (non-null String) 96 | :description "Link url"} 97 | :description {:type (non-null String) 98 | :description "Link description"} 99 | } 100 | :resolve :mutation/post!} 101 | 102 | :signup 103 | {:type (non-null :AuthPayload) 104 | :description "User registration" 105 | :args 106 | { 107 | :email {:type (non-null String) 108 | :description "User's email"} 109 | :password {:type (non-null String) 110 | :description "User's password"} 111 | :name {:type (non-null String) 112 | :description "User's name"} 113 | } 114 | :resolve :mutation/signup!} 115 | 116 | :login 117 | {:type :AuthPayload 118 | :description "User login" 119 | :args 120 | { 121 | :email {:type (non-null String) 122 | :description "User's email"} 123 | :password {:type (non-null String) 124 | :description "User's password"} 125 | } 126 | :resolve :mutation/login!} 127 | 128 | :vote 129 | {:type :Vote 130 | :description "User voting" 131 | :args 132 | { 133 | :link_id {:type (non-null ID) 134 | :description "User's email"} 135 | } 136 | :resolve :mutation/vote!} 137 | } 138 | 139 | :subscriptions 140 | { 141 | :newLink 142 | {:type :Link 143 | :stream :subscription/new-link} 144 | 145 | :newVote 146 | {:type :Vote 147 | :stream :subscription/new-vote} 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/cljs/hn_clj_pedestal_re_frame/events/graph_ql/mutations.cljs: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.events.graph-ql.mutations 2 | (:require 3 | [re-frame.core :as re-frame] 4 | [re-graph.core :as re-graph] 5 | [hn-clj-pedestal-re-frame.events.utils :as utils] 6 | [hn-clj-pedestal-re-frame.db :as db] 7 | [hn-clj-pedestal-re-frame.config :as config] 8 | [hn-clj-pedestal-re-frame.graph-ql.mutations :as mutations])) 9 | 10 | 11 | ;------------------------------------------------------------------------- 12 | ; Link Creation 13 | ;------------------------------------------------------------------------- 14 | 15 | (re-frame/reg-event-fx 16 | :create-link 17 | (fn [{:keys [db]} [_ description url]] 18 | {:db (-> db 19 | (assoc :loading? true) 20 | (assoc :error false)) 21 | :dispatch [::re-graph/mutate 22 | mutations/post 23 | {:url url 24 | :description description} 25 | [:on-create-link]]})) 26 | 27 | (re-frame/reg-event-fx 28 | :on-create-link 29 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 30 | {:db (-> db 31 | (assoc :loading? false) 32 | (assoc :link (:post data))) 33 | :dispatch [:navigate :home]})) 34 | 35 | 36 | ;------------------------------------------------------------------------- 37 | ; Link Votation 38 | ;------------------------------------------------------------------------- 39 | 40 | (re-frame/reg-event-fx 41 | :vote-link 42 | (fn [{:keys [db]} [_ link-id]] 43 | {:db (-> db 44 | (assoc :loading? true) 45 | (assoc :error false)) 46 | :dispatch [::re-graph/mutate 47 | mutations/vote 48 | {:link_id link-id} 49 | [:on-vote-link]]})) 50 | 51 | (re-frame/reg-event-fx 52 | :on-vote-link 53 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 54 | (let [vote-new (:vote data)] 55 | {:dispatch [:on-new-vote-db vote-new]}))) 56 | 57 | (re-frame/reg-event-db 58 | :on-new-vote-db 59 | (fn [db [_ vote-new]] 60 | (let [links (:new-links db) 61 | voted? (utils/voted? vote-new links)] 62 | (if voted? 63 | db 64 | (let [links-updated (utils/add-vote vote-new links)] 65 | (-> db 66 | (assoc :loading? false) 67 | (assoc :new-links links-updated))))))) 68 | 69 | 70 | ;------------------------------------------------------------------------- 71 | ; Signup 72 | ;------------------------------------------------------------------------- 73 | 74 | (re-frame/reg-event-fx 75 | :signup 76 | (fn [{:keys [db]} [_ name email password]] 77 | {:db (-> db 78 | (assoc :loading? true) 79 | (assoc :error false)) 80 | :dispatch [::re-graph/mutate 81 | mutations/signup 82 | {:email email 83 | :password password 84 | :name name} 85 | [:on-signup]]})) 86 | 87 | (re-frame/reg-event-fx 88 | :on-signup 89 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 90 | (let [token (get-in data [:signup :token]) 91 | authorization (str "Bearer " token) 92 | headers {"Authorization" authorization} 93 | http-parameters {:headers headers}] 94 | {:db (-> db 95 | (assoc :loading? false) 96 | (assoc-in 97 | config/token-header-path 98 | headers)) 99 | :dispatch [:navigate :home] 100 | :set-local-store ["token" token]}))) 101 | 102 | 103 | ;------------------------------------------------------------------------- 104 | ; Login 105 | ;------------------------------------------------------------------------- 106 | 107 | (re-frame/reg-event-fx 108 | :login 109 | (fn [{:keys [db]} [_ email password]] 110 | {:db (-> db 111 | (assoc :loading? true) 112 | (assoc :error false)) 113 | :dispatch [::re-graph/mutate 114 | mutations/login 115 | {:email email 116 | :password password} 117 | [:on-login]]})) 118 | 119 | (re-frame/reg-event-fx 120 | :on-login 121 | (fn [{:keys [db]} [_ {:keys [data errors] :as payload}]] 122 | (let [token (get-in data [:login :token]) 123 | authorization (str "Bearer " token) 124 | headers {"Authorization" authorization} 125 | http-parameters {:headers headers}] 126 | {:db (-> db 127 | (assoc :loading? false) 128 | (assoc-in 129 | config/token-header-path 130 | headers)) 131 | :set-local-store ["token" token] 132 | :dispatch [:navigate :home]}))) 133 | -------------------------------------------------------------------------------- /src/clj/hn_clj_pedestal_re_frame/schema.clj: -------------------------------------------------------------------------------- 1 | (ns hn-clj-pedestal-re-frame.schema 2 | "Contains custom resolvers and a function to provide the full schema." 3 | (:require 4 | [clojure.java.io :as io] 5 | [clojure.string :as string] 6 | [clojure.edn :as edn] 7 | [com.walmartlabs.lacinia.util :as util] 8 | [com.walmartlabs.lacinia.schema :as schema] 9 | [com.stuartsierra.component :as component] 10 | [buddy.hashers :as hs] 11 | [buddy.sign.jwt :as jwt] 12 | [io.pedestal.log :as log] 13 | [reagi.core :as r] 14 | [hn-clj-pedestal-re-frame.db :as db] 15 | [hn-clj-pedestal-re-frame.sql :as sql])) 16 | 17 | (def jwt-secret "GraphQL-is-aw3some") 18 | (def links-per-page 5) 19 | 20 | (def link-events (r/events)) 21 | (def vote-events (r/events)) 22 | 23 | (defn to-keyword 24 | [my-map] 25 | (into {} 26 | (for [[k v] my-map] 27 | [(keyword k) v]))) 28 | 29 | (defn get-user-id 30 | [context] 31 | (let [headers (to-keyword (get-in context [:request :headers])) 32 | authorization (:authorization headers) 33 | token (string/replace-first authorization #"Bearer " "") 34 | tuple (jwt/unsign token jwt-secret) 35 | user-id (:user-id tuple)] 36 | user-id)) 37 | 38 | (defn parse-order 39 | [input-key] 40 | (let [input-str (name input-key) 41 | input-list (string/split input-str #"_") 42 | criteria (nth input-list (dec (count input-list))) 43 | field (string/join "_" (drop-last input-list)) 44 | order {:field field 45 | :criteria criteria}] 46 | order)) 47 | 48 | (defn feed 49 | [db] 50 | (fn [_ args _] 51 | (let [{:keys [filter first skip]} args 52 | fltr (if (nil? filter) "" filter) 53 | fltr-sql (str "%" fltr "%" ) 54 | skp (if (nil? skip) 0 skip) 55 | frst (if (nil? first) links-per-page first) 56 | result (sql/filter-links-count {:filter fltr-sql} db) 57 | [first] result 58 | count (:count first) 59 | links (sql/filter-links {:? [frst skp] 60 | :filter fltr-sql} 61 | db) 62 | feed {:links links 63 | :count count}] 64 | feed))) 65 | 66 | (defn post! 67 | [db] 68 | (fn [context arguments _] 69 | (let [{:keys [url description]} arguments 70 | usr-id (get-user-id context) 71 | link (sql/insert-link (io/resource "hn-schema.edn") 177 | slurp 178 | edn/read-string 179 | (util/attach-resolvers (resolver-map component)) 180 | (util/attach-streamers (streamer-map component)) 181 | schema/compile)) 182 | 183 | (defrecord SchemaProvider [schema] 184 | 185 | component/Lifecycle 186 | 187 | (start [this] 188 | (assoc this :schema (load-schema this))) 189 | 190 | (stop [this] 191 | (assoc this :schema nil))) 192 | 193 | (defn new-schema-provider 194 | [] 195 | {:schema-provider (-> {} 196 | map->SchemaProvider 197 | (component/using [:db]))}) 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hn-clj-pedestal-re-frame 2 | 3 | Porting ["The Fullstack Tutorial for GraphQL"](https://www.howtographql.com), from Javascript to Clojure/script. 4 | 5 | I have tried to match each of the articles in the series in the git commit sequence, and the description given to each of them. 6 | 7 | More information to be shared in my [blog](https://promesante.github.io/2019/08/14/clojure_graphql_fullstack_learning_project_part_1.html). 8 | 9 | 10 | ## References ## 11 | 12 | * Port source: ["The Fullstack Tutorial for GraphQL"](https://www.howtographql.com) 13 | * Port target 14 | - Back-end: [Lacinia Pedestal Tutorial](https://lacinia.readthedocs.io/en/latest/tutorial/) 15 | - Front-end: 16 | * [re-frame docs](https://github.com/Day8/re-frame/blob/master/docs/README.md) 17 | * [re-frame tutorial](https://purelyfunctional.tv/guide/re-frame-building-blocks/) 18 | 19 | Instructions below assume having read these reference documents. 20 | 21 | ## Setup 22 | 23 | ### Database 24 | 25 | Database engine used is Postgresql. 26 | 27 | Database setup is performed by `setup-db.sh` script in the `bin` directory: 28 | 29 | ``` 30 | $ ./setup-db.sh 31 | create user hn_role password 'lacinia'; 32 | CREATE ROLE 33 | psql:setup-db.sql:1: NOTICE: table "link" does not exist, skipping 34 | DROP TABLE 35 | psql:setup-db.sql:2: NOTICE: table "usr" does not exist, skipping 36 | DROP TABLE 37 | psql:setup-db.sql:3: NOTICE: table "vote" does not exist, skipping 38 | DROP TABLE 39 | CREATE TABLE 40 | INSERT 0 2 41 | CREATE TABLE 42 | INSERT 0 2 43 | CREATE TABLE 44 | INSERT 0 3 45 | ``` 46 | 47 | Checking database has been setup right: 48 | 49 | ``` 50 | $ psql -h localhost -U hn_role hndb 51 | psql (11.4) 52 | Type "help" for help. 53 | 54 | hndb=> select * from usr; 55 | id | name | email | password | created_at | updated_at 56 | ----+------+------------------+----------+----------------------------+---------------------------- 57 | 1 | John | john@hotmail.com | john | 2019-08-14 16:22:03.508452 | 2019-08-14 16:22:03.508452 58 | 2 | Paul | paul@gmail.com | paul | 2019-08-14 16:22:03.508452 | 2019-08-14 16:22:03.508452 59 | (2 rows) 60 | 61 | hndb=> select * from link; 62 | id | description | url | usr_id | created_at | updated_at 63 | ----+------------------------------------------------------+-------------------------------------------+--------+----------------------------+---------------------------- 64 | 1 | INIT - Prisma turns your database into a GraphQL API | https://www.prismagraphql.com | 1 | 2019-08-14 16:22:03.514718 | 2019-08-14 16:22:03.514718 65 | 2 | INIT - The best GraphQL client | https://www.apollographql.com/docs/react/ | 2 | 2019-08-14 16:22:03.514718 | 2019-08-14 16:22:03.514718 66 | (2 rows) 67 | 68 | ``` 69 | 70 | 71 | ### Dependencies ### 72 | 73 | One of the key ones has been [re-graph][re-graph], one of the front-end, clojurescript GraphQL clients. 74 | 75 | In order to implement GraphQL subscriptions, I've had the following to issues: 76 | 77 | * [Subscription to Lacinia Pedestal back end: Getting just the first event][re-graph-issue1] 78 | * [Queries and Mutations: websockets or HTTP?][re-graph-issue2] 79 | 80 | To be able to go on, for each of them, I've implemented the workarounds depicted in those issues, and shared in my own [fork][re-graph-fork] of re-graph. In this fork, each of those workarounds has its own commit. Hence, re-graph dependency in this project references this fork. As its JAR file is not available online, in order to have this dependency resolved, this fork should be cloned and installed locally, running `lein install` in the cloned fork's root directory. 81 | 82 | 83 | ### GraphiQL ### 84 | 85 | On the server side, ["The Fullstack Tutorial for GraphQL"](https://www.howtographql.com) is based on [graphql-yoga](https://github.com/prisma/graphql-yoga) which, in turn, comes with [GraphQL Playground](https://github.com/prisma/graphql-playground) out of the box, as its “GraphQL IDE”. 86 | 87 | On the other hand, [Lacinia Pedestal](https://github.com/walmartlabs/lacinia-pedestal) comes with [GraphiQL](https://github.com/graphql/graphiql). So, we will use GraphiQL. 88 | 89 | When you need to access queries or mutations which require the user to be authenticated, whereas [GraphQL Playground](https://github.com/prisma/graphql-playground) lets you set the corresponding token in the IDE, [GraphiQL](https://github.com/graphql/graphiql) takes it as a configuration. 90 | 91 | Furthermore, [Lacinia Pedestal](https://github.com/walmartlabs/lacinia-pedestal) lets you set it as part of its own configuration, and hands it over to [GraphiQL](https://github.com/graphql/graphiql), in [server.clj](https://github.com/promesante/hn-clj-pedestal-re-frame/blob/master/src/clj/hn_clj_pedestal_re_frame/server.clj) as `:ide-headers`, as shown below: 92 | 93 | ```clojure 94 | (defrecord Server [schema-provider server port] 95 | 96 | component/Lifecycle 97 | (start [this] 98 | (assoc this :server (-> schema-provider 99 | :schema 100 | (lp/service-map 101 | {:graphiql true 102 | :ide-path "/graphiql" 103 | :port port 104 | :subscriptions true 105 | :ide-headers {:authorization "Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyLWlkIjozfQ.JH0Q2flkonyDPk_yiSrTK5VSKrbrsdR0FEePMgiEwDE"} 106 | }) 107 | (merge {::http/resource-path "/public"}) 108 | (add-route) 109 | http/create-server 110 | http/start))) 111 | ``` 112 | 113 | 114 | 115 | ### Starting Up ### 116 | 117 | In two different shell sessions: 118 | 119 | * Front-end: 120 | 121 | ``` 122 | $ lein clean 123 | $ lein figwheel 124 | Figwheel: Cutting some fruit, just a sec ... 125 | Figwheel: Validating the configuration found in project.clj 126 | Figwheel: Configuration Valid ;) 127 | Figwheel: Starting server at http://0.0.0.0:3449 128 | Figwheel: Watching build - dev 129 | Figwheel: Cleaning build - dev 130 | Compiling build :dev to "resources/public/js/compiled/app.js" from ["src/cljs"]... 131 | Successfully compiled build :dev to "resources/public/js/compiled/app.js" in 39.752 seconds. 132 | Figwheel: Starting CSS Watcher for paths ["resources/public/css"] 133 | Launching ClojureScript REPL for build: dev 134 | Figwheel Controls: 135 | (stop-autobuild) ;; stops Figwheel autobuilder 136 | (start-autobuild id ...) ;; starts autobuilder focused on optional ids 137 | (switch-to-build id ...) ;; switches autobuilder to different build 138 | (reset-autobuild) ;; stops, cleans, and starts autobuilder 139 | (reload-config) ;; reloads build config and resets autobuild 140 | (build-once id ...) ;; builds source one time 141 | (clean-builds id ..) ;; deletes compiled cljs target files 142 | (print-config id ...) ;; prints out build configurations 143 | (fig-status) ;; displays current state of system 144 | (figwheel.client/set-autoload false) ;; will turn autoloading off 145 | (figwheel.client/set-repl-pprint false) ;; will turn pretty printing off 146 | Switch REPL build focus: 147 | :cljs/quit ;; allows you to switch REPL to another build 148 | Docs: (doc function-name-here) 149 | Exit: :cljs/quit 150 | Results: Stored in vars *1, *2, *3, *e holds last exception object 151 | Prompt will show when Figwheel connects to your application 152 | [Rebel readline] Type :repl/help for online help info 153 | ClojureScript 1.10.238 154 | dev:cljs.user=> 155 | ``` 156 | 157 | * Back-end: 158 | 159 | If you have not started Postgresql yet: 160 | 161 | ``` 162 | $ pg_ctl -D /usr/local/var/postgres start 163 | ``` 164 | 165 | And then: 166 | 167 | ``` 168 | $ lein repl 169 | Retrieving cider/piggieback/0.4.0/piggieback-0.4.0.pom from clojars 170 | Retrieving cider/piggieback/0.4.0/piggieback-0.4.0.jar from clojars 171 | 172 | nREPL server started on port 60366 on host 127.0.0.1 - nrepl://127.0.0.1:60366 173 | REPL-y 0.4.3, nREPL 0.6.0 174 | Clojure 1.9.0 175 | Java HotSpot(TM) 64-Bit Server VM 1.8.0_192-b12 176 | Docs: (doc function-name-here) 177 | (find-doc "part-of-name-here") 178 | Source: (source function-name-here) 179 | Javadoc: (javadoc java-object-or-class-here) 180 | Exit: Control+D or (exit) or (quit) 181 | Results: Stored in vars *1, *2, *3, an exception in *e 182 | 183 | user=> (start) 184 | :started 185 | user=> 186 | ``` 187 | The application will be autommatically served in the last window of the browser you have last used. 188 | 189 | ### Shutting Down ### 190 | 191 | In the corresponding shell sessions mentioned in the previous section: 192 | 193 | * Front-end: 194 | 195 | ``` 196 | dev:cljs.user=> :cljs/quit 197 | $ 198 | ``` 199 | 200 | * Back-end: 201 | 202 | ``` 203 | user=> (stop) 204 | user=> (quit) 205 | Bye for now! 206 | ``` 207 | 208 | You could shut down Postgresql as well: 209 | 210 | ``` 211 | $ pg_ctl -D /usr/local/var/postgres stop 212 | ``` 213 | 214 | ## Usage ## 215 | 216 | The only not obvious functionalities are the ones implemented by means of GraphQL subscriptions: 217 | 218 | * new link submitted 219 | * voting an already existing link 220 | 221 | Those events are notified to every client by means of a GraphQL subscription. 222 | 223 | You can replicate these cases by means of GraphiQL, the GraphQL IDE supplied out of the box with Lacinia Pedestal, mentioned above, in the Setup section. 224 | 225 | You can access the back-end from two different tabs in your browser: 226 | 227 | * the application: `http://localhost:8888` 228 | * GraphiQL: `http://localhost:8888/graphiql` 229 | 230 | The latter should have been setup as depicted above. You can get the token mentioned there for configuration in the application: as you signup a new user, or login, in the browser's developer tools -> Application -> Local Storage -> "token" entry. 231 | 232 | A couple seconds after running each of these mutations in GraphiQL, the new link or vote will appear in the Hacker News application. 233 | 234 | GraphQL mutations: 235 | 236 | New Link: 237 | 238 | ```graphql 239 | mutation post($url:String!, $description:String!) { 240 | post( 241 | url: $url, 242 | description: $description 243 | ) { 244 | id 245 | } 246 | } 247 | ``` 248 | 249 | Parameters: 250 | 251 | ```json 252 | { 253 | "url": "https://simulacrum.party/posts/the-mutable-web/", 254 | "description": "The Mutable Web" 255 | } 256 | ``` 257 | 258 | Vote: 259 | 260 | ```graphql 261 | mutation vote($link_id:ID!) { 262 | vote( 263 | link_id: $link_id 264 | ) { 265 | id 266 | } 267 | } 268 | ``` 269 | 270 | Parameter: 271 | 272 | ```json 273 | { 274 | "link_id": 2 275 | } 276 | ``` 277 | 278 | [re-graph]: https://github.com/oliyh/re-graph "re-graph" 279 | 280 | [re-graph-issue1]: https://github.com/oliyh/re-graph/issues/42 "Subscription to Lacinia Pedestal back end: Getting just the first event" 281 | 282 | [re-graph-issue2]: https://github.com/oliyh/re-graph/issues/48 "Queries and Mutations: websockets or HTTP?" 283 | 284 | [re-graph-fork]: https://github.com/promesante/re-graph "re-graph fork" 285 | --------------------------------------------------------------------------------