├── .lein-failures ├── Procfile ├── .gitignore ├── src ├── cljs │ └── clojure_serverless_demo │ │ ├── db.cljs │ │ ├── config.cljs │ │ ├── subs.cljs │ │ ├── core.cljs │ │ ├── events.cljs │ │ └── views.cljs └── clj │ └── clojure_serverless_demo │ ├── aws │ ├── dynamodb_local.clj │ ├── lambda_simple.clj │ └── lambda.clj │ ├── config.clj │ ├── handler.clj │ ├── chat.clj │ ├── storage.clj │ ├── core.clj │ └── api.clj ├── resources ├── sqlite4java-libs │ ├── sqlite4java.jar │ ├── libsqlite4java-osx.dylib │ ├── sqlite4java-win32-x64.dll │ ├── sqlite4java-win32-x86.dll │ ├── libsqlite4java-linux-amd64.so │ └── libsqlite4java-linux-i386.so └── public │ └── index.html ├── test └── clj │ └── clojure_serverless_demo │ ├── core_test.clj │ ├── integration │ ├── dynamodb_local_test.clj │ └── http_test.clj │ ├── api_test.clj │ ├── fixtures.clj │ └── aws │ └── lambda_test.clj ├── bin └── deploy ├── README.md ├── serverless.yml └── project.clj /.lein-failures: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java $JVM_OPTS -cp target/clojure-serverless-demo.jar clojure.main -m clojure-serverless-demo.server 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /target 3 | /*-init.clj 4 | /resources/public/js/compiled 5 | out 6 | node_modules 7 | .serverless 8 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/db.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.db) 2 | 3 | (def default-db 4 | {:name nil 5 | :messages []}) 6 | -------------------------------------------------------------------------------- /resources/sqlite4java-libs/sqlite4java.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java.jar -------------------------------------------------------------------------------- /resources/sqlite4java-libs/libsqlite4java-osx.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-osx.dylib -------------------------------------------------------------------------------- /resources/sqlite4java-libs/sqlite4java-win32-x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java-win32-x64.dll -------------------------------------------------------------------------------- /resources/sqlite4java-libs/sqlite4java-win32-x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/sqlite4java-win32-x86.dll -------------------------------------------------------------------------------- /resources/sqlite4java-libs/libsqlite4java-linux-amd64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-linux-amd64.so -------------------------------------------------------------------------------- /resources/sqlite4java-libs/libsqlite4java-linux-i386.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuart-robinson/clojure-serverless-demo/HEAD/resources/sqlite4java-libs/libsqlite4java-linux-i386.so -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-serverless-demo.api :as api] 4 | [ring.mock.request :as mock])) 5 | -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lein clean 4 | 5 | #build and deploy front-end 6 | 7 | lein cljsbuild once min 8 | serverless client deploy -s dev 9 | 10 | #build and deploy back-end 11 | 12 | lein uberjar 13 | serverless deploy -s dev 14 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/config.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.config) 2 | 3 | (def debug? 4 | ^boolean goog.DEBUG) 5 | 6 | (def host 7 | (if debug? 8 | "http://localhost:8888" 9 | "https://odkas8ut1b.execute-api.eu-west-2.amazonaws.com/prod")) 10 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.subs 2 | (:require [re-frame.core :as re-frame])) 3 | 4 | (re-frame/reg-sub 5 | ::name 6 | (fn [db] 7 | (:name db))) 8 | 9 | (re-frame/reg-sub 10 | ::messages 11 | (fn [db] 12 | (:messages db))) 13 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/aws/dynamodb_local.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.aws.dynamodb-local 2 | (:import [com.amazonaws.services.dynamodbv2.local.main ServerRunner])) 3 | 4 | (System/setProperty "sqlite4java.library.path" 5 | (.getPath (clojure.java.io/resource "sqlite4java-libs"))); 6 | 7 | (def args (into-array String ["-inMemory"])) 8 | 9 | (defn build-server [] 10 | (ServerRunner/createServerFromCommandLineArgs args)) 11 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/config.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.config 2 | (:require [environ.core :refer [env]])) 3 | 4 | (def db-config {:access-key (env :x-aws-access-key-id) 5 | :secret-key (env :x-aws-secret-access-key) 6 | :endpoint (env :endpoint)}) 7 | 8 | (def table-config 9 | {:name (or (keyword (env :dynamodb-table-name)) :messages-dev) 10 | :primary-key [:id :s] 11 | :options {:throughput {:read 5 :write 1} 12 | :block? true}}) 13 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/handler.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.handler 2 | (:require [compojure.core :refer [GET defroutes]] 3 | [compojure.route :refer [resources]] 4 | [ring.util.response :refer [resource-response]] 5 | [ring.middleware.reload :refer [wrap-reload]])) 6 | 7 | (defroutes routes 8 | (GET "/" [] (resource-response "index.html" {:root "public"})) 9 | (resources "/")) 10 | 11 | (def dev-handler (-> #'routes wrap-reload)) 12 | 13 | (def handler routes) 14 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/chat.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.chat) 2 | 3 | (defn say [{:keys [name message]}] 4 | (let [timenow-ms (System/currentTimeMillis)] 5 | {:id (str (java.util.UUID/randomUUID)) 6 | :name name 7 | :message message 8 | :channel "default" 9 | :timestamp timenow-ms 10 | :order (- 2147483647 timenow-ms)})) 11 | 12 | (defn join [{:keys [name]}] 13 | (say {:name "channel" 14 | :message (str name " joined the channel...")})) 15 | 16 | (defn process-messages [messages] 17 | messages) 18 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | chat.stuart.cloud 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/aws/lambda_simple.clj: -------------------------------------------------------------------------------- 1 | ;;simple lambda function in clojure 2 | (ns clojure-serverless-demo.aws.lambda-simple 3 | (:require [cheshire.core :refer [generate-stream parse-stream generate-string]] 4 | [clojure.java.io :as io]) 5 | (:import [com.amazonaws.services.lambda.runtime.RequestStreamHandler]) 6 | (:gen-class 7 | :name clojure_serverless_demo.SimpleHandler 8 | :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler])) 9 | 10 | (defn -handleRequest 11 | [_ input-stream output-stream context] 12 | (with-open [writer (io/writer output-stream)] 13 | (generate-stream {:hello "world"} writer))) 14 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/core.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.core 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :as re-frame] 4 | [clojure-serverless-demo.events :as events] 5 | [clojure-serverless-demo.views :as views] 6 | [clojure-serverless-demo.config :as config])) 7 | 8 | (defn dev-setup [] 9 | (when config/debug? 10 | (enable-console-print!) 11 | (println "dev mode"))) 12 | 13 | (defn mount-root [] 14 | (re-frame/clear-subscription-cache!) 15 | (reagent/render [views/main-panel] 16 | (.getElementById js/document "app"))) 17 | 18 | (defn ^:export init [] 19 | (re-frame/dispatch-sync [::events/initialize-db]) 20 | (dev-setup) 21 | (mount-root)) 22 | -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/integration/dynamodb_local_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.integration.dynamodb-local-test 2 | (:require [clojure.test :refer :all] 3 | [taoensso.faraday :as far] 4 | [clojure-serverless-demo.fixtures :refer [with-local-db with-table]])) 5 | 6 | (def db-config {:access-key "ACCESSKEY" 7 | :secret-key "TOPSECRET" 8 | :endpoint "http://localhost:8000"}) 9 | 10 | (def table-config 11 | {:name :test-table 12 | :primary-key [:id :n] 13 | :throughput {:read 5 :write 1}}) 14 | 15 | (use-fixtures :once (with-local-db db-config) (with-table table-config db-config)) 16 | 17 | (deftest dynamodb-local-test 18 | (is (= (far/list-tables db-config) 19 | [:test-table]))) 20 | -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.api-test 2 | (:require [clojure-serverless-demo.api :as api] 3 | [clojure.test :refer :all] 4 | [ring.mock.request :as mock] 5 | [ring.middleware.json :refer [wrap-json-body]])) 6 | 7 | (def api (wrap-json-body (api/builder {}) {:keywords? true})) 8 | 9 | (deftest api-ping-test 10 | (is (= (api (mock/request :get "/ping")) 11 | {:status 200 12 | :headers {"Cache-Control" "max-age=0"} 13 | :body {:result "pong"}}))) 14 | 15 | (deftest api-echo-test 16 | (is (= (api (-> (mock/request :post "/echo") 17 | (mock/json-body {:foo "bar"}))) 18 | {:status 200 19 | :headers {"Cache-Control" "max-age=0"} 20 | :body {:foo "bar"}}))) 21 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/storage.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.storage 2 | (:require [taoensso.faraday :as far] 3 | [clojure-serverless-demo.config :as config])) 4 | 5 | ;;require secondary index inorder to be able to sort correctly 6 | ;; scan should use filter expression to discard messsage older than 10 minutes 7 | 8 | ;;require secondary index inorder to be able to sort correctly 9 | ;; scan should use filter expression to discard messsage older than 10 minutes 10 | (defn fetch-messages [db-config] 11 | (sort-by :timestamp (far/scan db-config 12 | (:name config/table-config) 13 | {:limit 10 14 | :span-reqs {:max 1}}))) 15 | 16 | (defn save-message [message db-config] 17 | (far/put-item db-config 18 | (:name config/table-config) 19 | message) 20 | {:result "success"}) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure-serverless-demo 2 | 3 | A full-stack clojure application deployed to API Gateway, AWS Lambda, S3 and Dynamodb 4 | 5 | WARNING: This application was built as a proof-of-concept and as such the code should not be considered production ready. 6 | 7 | ## Installation 8 | 9 | In order to deploy this application you will need to install the Serverless Framework using npm 10 | 11 | ``` 12 | # Install serverless framework 13 | 14 | npm install -g serverless 15 | 16 | # Install serverless S3 plugin 17 | 18 | npm install --save serverless-finch 19 | 20 | # Setup AWS Credentials 21 | 22 | aws configure 23 | ``` 24 | 25 | ## Tests 26 | 27 | ``` 28 | lein test 29 | ``` 30 | 31 | ## Deployment 32 | 33 | ### Front-end application: 34 | 35 | ``` 36 | lein clean 37 | lein cljsbuild once min 38 | serverless client deploy -s dev 39 | ``` 40 | 41 | ### Back-end application: 42 | 43 | ``` 44 | lein clean 45 | lein uberjar 46 | serverless deploy -s dev 47 | ``` 48 | 49 | 50 | ## Other Libraries 51 | 52 | * https://github.com/mhjort/ring-apigw-lambda-proxy - APIGateway Ring Middleware 53 | 54 | * https://github.com/uswitch/lambada - Clojure library for writing Lambda functions 55 | -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/fixtures.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.fixtures 2 | (:require [clojure.test :as t] 3 | [taoensso.faraday :as far] 4 | [ring.adapter.jetty :refer [run-jetty]] 5 | [clojure-serverless-demo.aws.dynamodb-local :as dynamodb-local])) 6 | 7 | (defn with-local-http [handler config] 8 | (fn [f] 9 | (let [server (run-jetty handler {:port (:port config) 10 | :join? false})] 11 | (try 12 | (f) 13 | (finally 14 | (.stop server)))))) 15 | 16 | (def client-opts {:access-key "ACCESSKEY" 17 | :secret-key "TOPSECRET" 18 | :endpoint "http://localhost:8000"}) 19 | 20 | (defn with-local-db [db-config] 21 | (fn [f] 22 | (let [s (dynamodb-local/build-server)] 23 | (try 24 | (.start s) 25 | (f) 26 | (finally 27 | (.stop s)))))) 28 | 29 | (defn with-table [table-config db-config] 30 | (fn [f] 31 | (far/ensure-table db-config 32 | (:name table-config) 33 | (:primary-key table-config) 34 | (:options table-config)) 35 | (f))) 36 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.core 2 | (:require [clojure-serverless-demo.aws.dynamodb-local :as local-db] 3 | [ring.adapter.jetty :refer [run-jetty]] 4 | [taoensso.faraday :as far] 5 | [clojure-serverless-demo.api :as api] 6 | [clojure-serverless-demo.config :as config])) 7 | 8 | (def default {:http-server nil 9 | :db-server nil}) 10 | 11 | (def server (atom default)) 12 | 13 | (defn run-local-server [] 14 | (let [db-config {:access-key "ACCESSKEY" 15 | :secret-key "TOPSECRET" 16 | :endpoint "http://localhost:8000"} 17 | api (api/handler (api/builder db-config)) 18 | table-config config/table-config 19 | db-server (local-db/build-server)] 20 | (swap! server assoc :db-server db-server 21 | :http-server (run-jetty api {:port 8888 :join? false})) 22 | (.start db-server) 23 | (far/ensure-table db-config 24 | (:name table-config) 25 | (:primary-key table-config) 26 | (:options table-config)))) 27 | 28 | (defn stop-local-server [] 29 | (.stop (:http-server @server)) 30 | (.stop (:db-server @server)) 31 | (reset! server default)) 32 | 33 | (defn -main [& args]) 34 | -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/aws/lambda_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.aws.lambda-test 2 | (:require [clojure-serverless-demo.aws.lambda :as sut] 3 | [clojure.test :refer :all])) 4 | 5 | (deftest api-gateway-request->ring-request-test 6 | (let [api-gateway-request 7 | 8 | {:path "/ping" 9 | :queryStringParameters nil 10 | :pathParameters {:proxy "ping"} 11 | :headers {:X-Forwarded-Proto "https" 12 | :X-Forwarded-Port "443" 13 | :X-Forwarded-For "81.106.81.0, 54.182.244.13" 14 | :Host "kofu3jdkgj.execute-api.us-east-1.amazonaws.com" 15 | :User-Agent "curl/7.55.1"} 16 | :resource "/{proxy+}" 17 | :httpMethod "GET" 18 | :requestContext {:path "/dev/ping" 19 | :stage "dev" 20 | :protocol "HTTP/1.1" 21 | :resourceId "suovu9" 22 | :requestTime "17/Apr/2018:21:17:59 +0000" 23 | :requestId "ce486fc6-4284-11e8-951d-b96ffe0b5d52" 24 | :httpMethod "GET"} 25 | :body "test" 26 | :query-string "test"} 27 | 28 | 29 | result (sut/api-gateway-request->ring-request api-gateway-request)] 30 | (is (= 1 1)))) 31 | -------------------------------------------------------------------------------- /test/clj/clojure_serverless_demo/integration/http_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.integration.http-test 2 | (:require [clojure-serverless-demo.fixtures :as f] 3 | [clojure-serverless-demo.api :as api] 4 | [clojure-serverless-demo.config :as config] 5 | [clj-http.client :as http] 6 | [clojure.test :refer :all])) 7 | 8 | (def http-config 9 | {:port 8888}) 10 | 11 | (def db-config {:access-key "ACCESSKEY" 12 | :secret-key "TOPSECRET" 13 | :endpoint "http://localhost:8000"}) 14 | 15 | (def api (api/handler (api/builder db-config))) 16 | 17 | (def http-addr (str "http://localhost:" (:port http-config))) 18 | 19 | (use-fixtures :once 20 | (f/with-local-http api http-config) 21 | (f/with-local-db db-config) 22 | (f/with-table config/table-config db-config)) 23 | 24 | (deftest api-ping-test 25 | (is (= (:body (http/get (str http-addr "/ping") {:as :auto})) 26 | {:result "pong"}))) 27 | 28 | (deftest api-join-request-test 29 | (http/post (str http-addr "/join") 30 | {:form-params {:name "test-name"} 31 | :content-type :json}) 32 | (http/post (str http-addr "/say") 33 | {:form-params {:name "test-name" 34 | :message "hello world"} 35 | :content-type :json}) 36 | (let [results (:body (http/get 37 | (str http-addr "/fetch-messages") 38 | {:as :auto}))] 39 | (is (= ["channel" "test-name"] (map :name results))))) 40 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/api.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.api 2 | (:require [compojure.core :refer [GET POST defroutes]] 3 | [ring.middleware.cors :refer [wrap-cors]] 4 | [clojure-serverless-demo.chat :as chat] 5 | [clojure-serverless-demo.storage :as storage] 6 | [ring.middleware.keyword-params :refer [wrap-keyword-params]] 7 | [ring.middleware.json :refer [wrap-json-response wrap-json-body]])) 8 | 9 | (defn response [body status max-age] 10 | {:status status 11 | :body body 12 | :headers {"Cache-Control" (str "max-age=" max-age)}}) 13 | 14 | (defn builder [db-config] 15 | (defroutes api 16 | (GET "/ping" [] 17 | (response {:result "pong"} 200 0)) 18 | 19 | (POST "/echo" {:keys [body] :as request} 20 | (response body 200 0)) 21 | 22 | (GET "/fetch-messages" [] 23 | (-> (storage/fetch-messages db-config) 24 | (chat/process-messages) 25 | (response 200 0))) 26 | 27 | (POST "/join" {:keys [body] :as request} 28 | (-> (chat/join body) 29 | (storage/save-message db-config) 30 | (response 200 0))) 31 | 32 | (POST "/say" {:keys [body] :as request} 33 | (-> (chat/say body) 34 | (storage/save-message db-config) 35 | (response 200 0))))) 36 | 37 | (defn handler [api] 38 | (-> (wrap-cors api :access-control-allow-origin [#".*"] 39 | :access-control-allow-methods [:get :put :post :delete]) 40 | (wrap-json-body {:keywords? true}) 41 | (wrap-json-response))) 42 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: clojure-serverless-demo 2 | 3 | provider: 4 | name: aws 5 | runtime: java8 6 | stage: ${opt:stage, 'dev'} 7 | region: eu-west-2 8 | 9 | # Frontend Deployment Config 10 | plugins: 11 | - serverless-finch 12 | 13 | custom: 14 | client: 15 | bucketName: clojure-serverless-demo-london-${opt:stage, 'dev'} 16 | distributionFolder: resources/public/ 17 | 18 | # Backend Deployment Config 19 | package: 20 | artifact: target/clojure-serverless-demo-standalone.jar 21 | 22 | functions: 23 | simplehandler: 24 | handler: clojure_serverless_demo.SimpleHandler 25 | 26 | apihandler: 27 | handler: clojure_serverless_demo.ApiHandler 28 | events: 29 | - http: 30 | path: /{path} 31 | method: ANY 32 | 33 | resources: 34 | Resources: 35 | DynamoDbTable: 36 | Type: AWS::DynamoDB::Table 37 | Properties: 38 | TableName: messages-${self:provider.stage} 39 | AttributeDefinitions: 40 | - AttributeName: channel 41 | AttributeType: S 42 | - AttributeName: order 43 | AttributeType: N 44 | KeySchema: 45 | - AttributeName: channel 46 | KeyType: HASH 47 | - AttributeName: order 48 | KeyType: RANGE 49 | ProvisionedThroughput: 50 | ReadCapacityUnits: 20 51 | WriteCapacityUnits: 5 52 | DynamoDBIamPolicy: 53 | Type: AWS::IAM::Policy 54 | DependsOn: DynamoDbTable 55 | Properties: 56 | PolicyName: lambda-dynamodb-${self:provider.stage} 57 | PolicyDocument: 58 | Version: '2012-10-17' 59 | Statement: 60 | - Effect: Allow 61 | Action: 62 | - dynamodb:* 63 | Resource: arn:aws:dynamodb:*:*:table/messages-${self:provider.stage} 64 | Roles: 65 | - Ref: IamRoleLambdaExecution 66 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/events.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.events 2 | (:require [re-frame.core :as re-frame] 3 | [clojure-serverless-demo.db :as db] 4 | [clojure-serverless-demo.config :as config] 5 | [ajax.core :refer [GET POST]])) 6 | 7 | (re-frame/reg-event-db 8 | ::initialize-db 9 | (fn [_ _] 10 | db/default-db)) 11 | 12 | (re-frame/reg-event-db 13 | ::user-submitted-message 14 | (fn [db [_ message]] 15 | (POST 16 | (str config/host "/say") 17 | {:params {:name (:name db) 18 | :message message} 19 | :format :json 20 | :response-format :json 21 | :keywords? true 22 | :handler #(re-frame/dispatch [::process-say-response %]) 23 | :error-handler #(re-frame/dispatch [::bad-response %])}) 24 | db)) 25 | 26 | (re-frame/reg-event-db 27 | ::user-submitted-username 28 | (fn [db [_ name]] 29 | (POST 30 | (str config/host "/join") 31 | {:params {:name name} 32 | :format :json 33 | :response-format :json 34 | :keywords? true 35 | :handler #(re-frame/dispatch [::process-join-response % name]) 36 | :error-handler #(re-frame/dispatch [::bad-response %])}) 37 | db)) 38 | 39 | (re-frame/reg-event-db 40 | ::process-join-response 41 | (fn [db [_ response name]] 42 | (re-frame/dispatch [::fetch-messages true]) 43 | (assoc db :name name))) 44 | 45 | (re-frame/reg-event-db 46 | ::fetch-messages 47 | (fn [db [_ cached?]] 48 | (let [] 49 | (GET 50 | (str config/host "/fetch-messages") 51 | {:response-format :json 52 | :keywords? true 53 | :handler #(re-frame/dispatch [::process-fetch-messages-response %]) 54 | :error-handler #(re-frame/dispatch [::bad-response %])} 55 | )) 56 | db)) 57 | 58 | (re-frame/reg-event-db 59 | ::process-say-response 60 | (fn [db [_ response]] 61 | (re-frame/dispatch [::fetch-messages true]) 62 | db)) 63 | 64 | (re-frame/reg-event-db 65 | ::process-fetch-messages-response 66 | (fn [db [_ response]] 67 | (assoc db :messages response))) 68 | 69 | 70 | (re-frame/reg-event-db 71 | ::bad-response 72 | (fn [db [_ response]] 73 | (.debug js/console response) 74 | db)) 75 | 76 | 77 | (defn poll-for-new-messages [interval] 78 | (js/setInterval #(re-frame/dispatch [::fetch-messages false]) interval)) 79 | 80 | 81 | (re-frame.core/reg-fx 82 | ::Interval 83 | (poll-for-new-messages 1000)) 84 | -------------------------------------------------------------------------------- /src/clj/clojure_serverless_demo/aws/lambda.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.aws.lambda 2 | (:require [clojure-serverless-demo.api :as api] 3 | [clojure-serverless-demo.config :as config] 4 | [cheshire.core :refer [generate-stream parse-stream generate-string]] 5 | [clojure.java.io :as io]) 6 | 7 | (:import [com.amazonaws.services.lambda.runtime.RequestStreamHandler]) 8 | (:gen-class 9 | :name clojure_serverless_demo.ApiHandler 10 | :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler])) 11 | 12 | (defn api-gateway-request->ring-request 13 | "coerce an API gateway request to a valid Ring request" 14 | [request] 15 | {:server-port (-> (:headers request) :X-Forwarded-Port Integer/parseInt) 16 | :server-name (:Host (:headers request)) 17 | :remote-addr (-> (:headers request) 18 | :X-Forwarded-For 19 | (clojure.string/split #", ") 20 | first) 21 | :uri (:path request) 22 | :scheme (-> (:headers request) :X-Forwarded-Proto keyword) 23 | :protocol (:protocol (:requestContext request)) 24 | :headers (into {} (for [[k v] (:headers request)] 25 | [(clojure.string/lower-case (name k)) v])) 26 | :request-method (-> (:httpMethod request) 27 | (clojure.string/lower-case) 28 | (keyword)) 29 | :body (when-let [body (:body request)] (io/input-stream (.getBytes body))) 30 | :query-string (:queryStringParameters request)}) 31 | 32 | (defn ring-response->api-gateway-response 33 | "coerce a Ring response to an API gateway response" 34 | [response] 35 | {:statusCode (str (:status response)) 36 | :body (:body response) 37 | :headers (:headers response)}) 38 | 39 | (defn wrap-api-gateway-request [f] 40 | (fn [request] 41 | (-> (api-gateway-request->ring-request request) 42 | (f) 43 | (ring-response->api-gateway-response)))) 44 | 45 | 46 | (def api-gateway-handler (-> (api/builder config/db-config) 47 | (api/handler) 48 | (wrap-api-gateway-request))) 49 | 50 | (defn -handleRequest 51 | [_ input-stream output-stream context] 52 | (with-open [writer (io/writer output-stream)] 53 | (let [request (parse-stream (io/reader input-stream) true) 54 | logger (.getLogger context) 55 | _ (.log logger (str request)) 56 | _ (.log logger (str (merge config/db-config config/table-config))) 57 | response (api-gateway-handler request)] 58 | (.log logger (str response)) 59 | (generate-stream response writer)))) 60 | -------------------------------------------------------------------------------- /src/cljs/clojure_serverless_demo/views.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-serverless-demo.views 2 | (:require [re-frame.core :as re-frame] 3 | [clojure-serverless-demo.subs :as subs] 4 | [clojure-serverless-demo.events :as events] 5 | [reagent.core :as r])) 6 | 7 | (defn user-message-input [] 8 | (let [message (r/atom "") 9 | ok-click (fn [event] 10 | (.preventDefault event) 11 | (when-not (empty? @message) 12 | (re-frame/dispatch [::events/user-submitted-message @message]) 13 | (reset! message ""))) 14 | fetch-click (fn [_] 15 | (re-frame/dispatch [::events/fetch-messages "foo"]))] 16 | (fn [] 17 | [:form 18 | [:div.box.columns 19 | [:div.column.is-four-fifths 20 | [:input.input.is-primary {:type "text" 21 | :placeholder " something..." 22 | :value @message 23 | :on-change #(reset! message (-> % .-target .-value))}]] 24 | [:div.column.is-one-fifth 25 | [:button.button.is-primary.is-rounded 26 | {:type "input" 27 | :on-click #(ok-click %) } 28 | "Send"]]]]))) 29 | 30 | (defn user-join-input [] 31 | (let [name (r/atom "") 32 | ok-click (fn [event] 33 | (.preventDefault event) 34 | (when-not (empty? @name) 35 | (re-frame/dispatch [::events/user-submitted-username @name]) 36 | (reset! name "")))] 37 | (fn [] 38 | [:form 39 | [:div.box.columns 40 | [:div.column.is-one-fifth 41 | [:input.input {:type "text" 42 | :placeholder "enter username" 43 | :value @name 44 | :on-change #(reset! name (-> % .-target .-value))}]] 45 | [:div.column.is-one-fifth 46 | [:span 47 | [:button.button.is-primary {:type "input" 48 | :on-click #(ok-click %) } 49 | "Join"] 50 | ]]]]))) 51 | 52 | 53 | (defn message-line [message] 54 | [:li.box 55 | [:span.has-text-weight-bold (:name message)] 56 | [:span " "] 57 | [:span (:message message)]]) 58 | 59 | (defn message-panel [] 60 | (let [messages (re-frame/subscribe [::subs/messages])] 61 | (fn [] 62 | [:div 63 | (into [:ol.chat] (map message-line @messages))]))) 64 | 65 | (defn join-panel [] 66 | (fn [] 67 | [user-join-input])) 68 | 69 | (defn main-panel [] 70 | (let [name (re-frame/subscribe [::subs/name])] 71 | (fn [] 72 | [:div#main.has-background-grey-lighter 73 | [:div.navbar.is-fixed-top.is-primary 74 | [:span.navbar-item.has-text-white.has-text-weight-bold "Chat"]] 75 | (if (nil? @name) 76 | [join-panel] 77 | [:div 78 | [:section.section.has-background-grey-lighter 79 | [message-panel]] 80 | [:section.section 81 | [user-message-input]]])]))) 82 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-serverless-demo "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.8.0"] 3 | [org.clojure/clojurescript "1.9.908"] 4 | [reagent "0.7.0"] 5 | [re-frame "0.10.5"] 6 | [cljs-ajax "0.5.1"] 7 | [com.amazonaws/aws-lambda-java-core "1.0.0"] 8 | [clj-http "3.7.0"] 9 | [compojure "1.6.0"] 10 | [environ "1.1.0"] 11 | [ring "1.6.3"] 12 | [ring-cors "0.1.12"] 13 | [ring/ring-mock "0.3.2"] 14 | [ring/ring-json "0.4.0"] 15 | [ring/ring-jetty-adapter "1.6.3"] 16 | [com.amazonaws/DynamoDBLocal "1.10.5.1"] 17 | [com.taoensso/faraday "1.9.0"]] 18 | 19 | :repositories [["dynamodblocal" {:url "https://s3-us-west-2.amazonaws.com/dynamodb-local/release"}]] 20 | 21 | :plugins [[lein-cljsbuild "1.1.5"]] 22 | 23 | :min-lein-version "2.5.3" 24 | 25 | :source-paths ["src/clj" "test/clj"] 26 | 27 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 28 | 29 | :figwheel {:css-dirs ["resources/public/css"] 30 | :ring-handler clojure-serverless-demo.handler/dev-handler} 31 | 32 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 33 | 34 | :profiles 35 | {:uberjar {:aot :all 36 | :uberjar-name "clojure-serverless-demo-standalone.jar"} 37 | :dev 38 | {:dependencies [[binaryage/devtools "0.9.4"] 39 | [day8.re-frame/re-frame-10x "0.3.0"] 40 | [day8.re-frame/tracing "0.5.0"] 41 | [figwheel-sidecar "0.5.13"] 42 | [com.cemerick/piggieback "0.2.2"]] 43 | 44 | :plugins [[lein-figwheel "0.5.13"]]} 45 | :prod { :dependencies [[day8.re-frame/tracing-stubs "0.5.0"]]}} 46 | 47 | :cljsbuild 48 | {:builds 49 | [{:id "dev" 50 | :source-paths ["src/cljs"] 51 | :figwheel {:on-jsload "clojure-serverless-demo.core/mount-root"} 52 | :compiler {:main clojure-serverless-demo.core 53 | :output-to "resources/public/js/compiled/app.js" 54 | :output-dir "resources/public/js/compiled/out" 55 | :asset-path "js/compiled/out" 56 | :source-map-timestamp true 57 | :preloads [devtools.preload 58 | day8.re-frame-10x.preload] 59 | :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true 60 | "day8.re_frame.tracing.trace_enabled_QMARK_" true} 61 | :external-config {:devtools/config {:features-to-install :all}} 62 | }} 63 | 64 | {:id "min" 65 | :source-paths ["src/cljs"] 66 | :jar true 67 | :compiler {:main clojure-serverless-demo.core 68 | :output-to "resources/public/js/compiled/app.js" 69 | :optimizations :advanced 70 | :closure-defines {goog.DEBUG false} 71 | :pretty-print false}} 72 | 73 | 74 | ]} 75 | 76 | :main clojure-serverless-demo.core 77 | 78 | 79 | ;; :prep-tasks [["cljsbuild" "once" "min"] "compile"] 80 | ) 81 | --------------------------------------------------------------------------------