├── 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 |
--------------------------------------------------------------------------------