diff --git a/examples/http-server/.env b/examples/http-server/.env new file mode 100644 index 00000000..97a92a2d --- /dev/null +++ b/examples/http-server/.env @@ -0,0 +1,7 @@ +DB_HOST=localhost +DB_USER=postgres +DB_PASS=secret +DB_PORT=5432 +DB_NAME=postgres +HTTP_HOST=localhost +HTTP_PORT=3000 \ No newline at end of file diff --git a/examples/http-server/README.md b/examples/http-server/README.md new file mode 100644 index 00000000..a5ce548d --- /dev/null +++ b/examples/http-server/README.md @@ -0,0 +1,37 @@ +## HTTP server example + +An example server that queries a `users` table in Postgres and returns results either as JSON or HTML. + +### start the db + +`docker-compose up` + +### create a table for the app + +```sql +create table if not exists users +( + id text primary key, + first_name text, + last_name text +); + +insert into users (id, first_name, last_name) +values ('foo', 'Bob', 'Bobberton'); +``` + +### install modules + +`npm install` + +### develop with the nREPL + +1. start nREPL server `npx squint nrepl-server :port 1888` +2. connect an editor + +### compile and run + +``` +npx squint compile +node target/server.js +``` diff --git a/examples/http-server/docker-compose.yml b/examples/http-server/docker-compose.yml new file mode 100644 index 00000000..94c565a3 --- /dev/null +++ b/examples/http-server/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.9' + +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_PASSWORD: secret + POSTGRES_USER: postgres + POSTGRES_DB: postgres + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + +volumes: + db_data: \ No newline at end of file diff --git a/examples/http-server/package.json b/examples/http-server/package.json new file mode 100644 index 00000000..aa9bc03e --- /dev/null +++ b/examples/http-server/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "pg": "8.13.1", + "dotenv": "14.0.0" + }, + "devDependencies": { + "squint-cljs": "^0.8.123" + } + } \ No newline at end of file diff --git a/examples/http-server/squint.edn b/examples/http-server/squint.edn new file mode 100644 index 00000000..a540806f --- /dev/null +++ b/examples/http-server/squint.edn @@ -0,0 +1,3 @@ +{:paths ["src"] + :output-dir "target" + :extension "js"} \ No newline at end of file diff --git a/examples/http-server/src/server.cljs b/examples/http-server/src/server.cljs new file mode 100644 index 00000000..d898b9f6 --- /dev/null +++ b/examples/http-server/src/server.cljs @@ -0,0 +1,96 @@ +(ns server + (:require + ["dotenv" :as dotenv] + ["http" :as http] + ["url" :as url] + ["pg$default" :as pg])) + +(.config dotenv) +(def Pool pg.Pool) + +(defonce pool + (Pool. + {:host (.. js/process -env -DB_HOST) + :port (.. js/process -env -DB_PORT) + :database (.. js/process -env -DB_NAME) + :user (.. js/process -env -DB_USER) + :password (.. js/process -env -DB_PASS)})) + +(defn ^:async execute-query [query params] + (.-rows (js-await (.query pool query params)))) + +(def favicon + (str + "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAE" + (apply str (take 1714 (repeat "A"))) + "P//AAD8fwAA/H8AAPxjAAD/4wAA/+MAAMY/AADGPwAAxjEAAP/xAAD/8QAA4x8AAOMfAADjHwAA//8AAP//AAA=")) + +(defn ^:async home-page [_req] + (.then + (execute-query "select * from users" []) + (fn [users] + (str + "\n" + #html + [:html + [:head + [:link {:href favicon :rel "icon" :type "image/x-icon"}] + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + [:title "Welcome to Squint"] + [:link {:href "https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css" :rel "stylesheet"}]] + [:body + [:section.section + [:div.content + [:h2 [:b "Welcome to Squint"]] + [:ul + (doall + (for [{:keys [first_name last_name]} users] + #html [:li last_name "," first_name]))]]]]])))) + +(defn ^:async search-handler [{:keys [url]}] + (let [id (-> (url/parse url true) (.-query) :id)] + (.then + (execute-query "select * from users where id = $1" [id]) + (fn [users] + (js/JSON.stringify #js {:users users}))))) + +(defn respond [req res {:keys [handler content-type status]}] + (-> (handler req) + (.then (fn [body] + (set! (.-statusCode res) status) + (.setHeader res "Content-Type" content-type) + (.end res body))) + (.catch (fn [error] + (js/console.error error) + (set! (.-statusCode res) 500) + (.setHeader res "Content-Type" "text/plain") + (.end res "an error occurred serving the request"))))) + +(defn handler [{:keys [url] :as req} res] + (let [parsed-url (url/parse url true) + path (.-pathname parsed-url) + request-method (.-method req)] + (respond + req res + (cond + (and (= "GET" request-method) (= "/" path)) + {:handler home-page + :status 200 + :content-type "text/html; charset=utf-8"} + (and (= "GET" request-method) (.startsWith path "/search")) + {:handler search-handler + :status 200 + :content-type "application/json; charset=utf-8"} + :else + {:handler (^:async fn [] + (js/Promise.resolve + (str "requested route " request-method " " path " not found"))) + :status 404 + :content-type "text/plain"})))) + +(defn start-server [host port] + (let [server (http/createServer handler)] + (.listen server port host (fn [] (println "server started on" host port ))))) + +(start-server (.. js/process -env -HTTP_HOST) (.. js/process -env -HTTP_PORT)) \ No newline at end of file