A Clojure library that provides:
- A web framework based around Jetty and Ring
- Utility scripts for managing databases, email, logging, SSL certs, systemd services, and more
- Utility namespaces with functions for common CLI and web server programming tasks
- Add the library’s coordinates to the
:deps
map in your project’sdeps.edn
file:sig-gis/triangulum {:git/url "https://github.com/sig-gis/triangulum" :git/sha "REPLACE-WITH-CURRENT-SHA"}
If you want to pull Triangulum into a Babashka script, you can do so with
babashka.deps
as follows:(require '[babashka.deps :refer [add-deps]]) (add-deps '{:deps {sig-gis/triangulum {:git/url "https://github.com/sig-gis/triangulum" :git/sha "REPLACE-WITH-CURRENT-SHA"}}})
- Define one or more aliases for Triangulum’s utility scripts in the
:aliases
map in your project’sdeps.edn file
.NOTE: You can find examples of these aliases in the
deps.example.edn
file in the root directory of this repository. - Configure the Triangulum features you want to use in a
config.edn
file in the root directory of your repository.Two syntaxes are provided for this file, one with nested keywords and one with namespaced keywords. You can see examples of the available features in
config.nested-example.edn
andconfig.namespaced-example.edn
within this repository.
To launch the test suite, run:
clojure -M:test
This namespace provides a batteries-included Ring Jetty web server, an nrepl server for connecting to the live web application, a timestamped file logging system, and worker functions for non-HTTP-related tasks.
- In config.edn:
:server {:http-port 8080 :https-port 8443 ; Only if you also include the keystore fields below :nrepl-bind 127.0.0.1 ; Must be paired with either :nrepl or :cider-repl below :nrepl-port 5555 ; Must be paired with either :nrepl or :cider-repl below :nrepl false ; For a vanilla nrepl :cider-nrepl true ; If your editor supports CIDER middleware :mode "dev" ; or prod :log-dir "logs" ; or "" for stdout :truncate-request true ; false by default :handler product-ns.routing/handler :workers {:scheduler {:start product-ns.jobs/start-scheduled-jobs! :stop product-ns.jobs/stop-scheduled-jobs!}} :response-type :json ; #{:json :edn :transit} :bad-tokens #{".php"} ; Reject URLs containing these strings :keystore-file "keystore.pkcs12" ; Contains your SSL certificate(s) :keystore-type "pkcs12" :keystore-password "foobar"}
- At the command line:
Start the server:
clojure -M:server start
Stop the server (requires
:nrepl
or:cider-nrepl
):clojure -M:server stop
Reload your namespaces into the running JVM (requires
:nrepl
or:cider-nrepl
):clojure -M:server reload
NOTES:
- To use
:https-port
, you must also specify:keystore-file
,:keystore-type
, and:keystore-password
. - You may not use both
:nrepl
and:cider-nrepl
at the same time. - The
:start
functions for all:workers
will be run before the web server is started. - The
:stop
functions for all:workers
will be run after the web server is stopped. They will receive the outputs of the:start
functions. - Before starting the server, you must set
:handler
to a namespace-qualified symbol bound to a Ring handler function. - If you set
:mode
to “dev”, then thewrap-reload
middleware will be added to your handler function. This will automatically reload all of your namespaces into the running JVM on each HTTP request.
- To use
The triangulum.handler
namespace provides core request handling and
middleware composition for Triangulum applications. It sets up a Ring
handler stack that includes various middlewares, such as
request/response logging, exception handling, and request parameter
parsing. Optional middlewares like wrap-ssl-redirect
and
wrap-reload
can be applied based on your config.edn
settings.
triangulum.handler/authenticated-routing-handler
is an optional generic Ring
compliant routing handler that allows your project to provide custom authentication,
redirection, and routing behavior based on your config.edn
settings.
If you need to provide a symbol that is bound to a handler function to
figwheel-main
, you can use triangulum.handler/development-app
to
load in your project’s handler function from config.edn
with the
standard Triangulum middlewares added.
If you choose to use triangulum.handler/authenticated-routing-handler
:
- Set
:handler
totriangulum.handler/authenticated-routing-handler
.
;; nested config
{:server {:handler triangulum.handler/authenticated-routing-handler}}
;; namespaced config
{:triangulum.server/handler triangulum.handler/authenticated-routing-handler}
triangulum.handler/authenticated-routing-handler
is designed to optionally support being provided multiple route maps, so that you can compose app specific routes with generic routes provided by common libraries.To configure which route maps are used, provide
:routing-tables
with a vector of one or multiple route maps in yourconfig.edn
;; nested config
{:server {:routing-tables [common-libary-ns.routing/routes product-ns.routing/routes]}}
;; namespaced config
{:triangulum.handler/routing-tables [common-libary-ns.routing/routes product-ns.routing/routes]}
Note that authenticated-routing-handler
will merge this vector of route maps into
one; this enables you to place route maps that provide specific implementations of
routes to the right of common libraries that provide generic implentations as a way
of overriding defaults.
triangulum.handler/authenticated-routing-handler
will use any custom implementation of these handlers that you specify in yourconfig.edn
::not-found-handler
(args:request
), called when no corresponding route is found.:route-authenticator
(args:request
, route:auth-type
), determines if client is authenticated:redirect-handler
(args:request
), called when client is not authenticated
;; nested config
{:server {:not-found-handler product-ns.handlers/not-found-handler
:redirect-handler product-ns.handlers/redirect-handler
:route-authenticator product-ns.handlers/route-authenticator}}
;; namespaced config
{:triangulum.handler/not-found-handler product-ns.handlers/not-found-handler
:triangulum.handler/redirect-handler product-ns.handlers/redirect-handler
:triangulum.handler/route-authenticator product-ns.handlers/route-authenticator}
This namespace provides functions for rendering pages and handling resources in Triangulum. It defines functions for reading asset files, generating HTML, and handling various types of responses.
- Require the namespace in your project.
- Use
render-page
to generate a handler that will return an HTML response with a standard template for React/Reagent web apps.
(ns my-app.views
(:require [triangulum.views :refer [render-page]]))
(def my-page-handler (render-page "/my-page"))
[uri]
Returns a function that takes a request and generates the HTML for the specified URI using the request’s parameters and session data. The generated HTML includes the necessary head and body sections.
Example usage: (def my-page (render-page “/my-page”))
In JavaScript projects, we assign the relative path (from the project root)
to the main component JSX file to the :js-init
key. This file should export
a function called pageInit
that expects two arguments: params
and session
.
You only need to set this key for the development mode to work,
which enables Vite hot reload. In production, we rely on a manifest file
generated by the bundling process to find the entry point. However, we still
need to define pageInit
and export it in the main entry point file.
In ClojureScript projects, we need to assign the namespaced symbol of the init
function to the :cljs-init
key, which accepts params
and session
as
arguments, for both production and development environments.
The session
map will also contain the :client-keys
that were added in
Triangulum’s config.edn
.
;; nested config
{:app {:client-keys {:token "client-token" }}}
;; namespaced config
{:triangulum.views/client-keys {:token "client-token" }}}
You can provide :tags-url
, which is a url to the git tags page of
your repository. Triangulum will extract all tags beginning with
“prod”, sort them lexicographically, and return the last entry. If you
use tags of the form “prod-YYYY.MM.DD-HASH”, then this will return the
one with the latest date.
This tag label will be passed to the browser code in the :session
map under the :versionDeployed
key.
To set up the folder and file structure for use with build-db
, use the following directory structure:
src/
|___clj/
| |___<project namespace>
|
|___cljs/
| |___<project namespace>
|
|___sql/
|___create_db.sql
|___changes/
|___default_data/
|___dev_data/
|___functions/
|___tables/
You may also run this command in your project root directory:
mkdir -p src/sql/{changes,default_data,dev_data,functions,tables}
Postgresql needs to be installed on the machine that will be hosting this website. This installation task is system specific and is beyond the scope of this README, so please follow the instructions for your operating system and Postgresql version. However, please ensure that the database server’s superuser account is named “postgres” and that you know its database connection password before proceeding.
Once the Postgresql database server is running on your machine, you
should navigate to the top level directory (i.e., the directory
containing this README) and add the following alias to your deps.edn
file:
{:aliases {:build-db {:main-opts ["-m" "triangulum.build-db"]}}}
Then run the database build command as follows:
clojure -M:build-db build-all -d database [-u user] [-p admin password]
This will call ./src/sql/create_db.sql
, stored in the individual project
repository. A variable database
is set for the command line call to
create_db.sql. This allows your project to generate the project database
with a different name, depending on your deployment. To use this variable
type :database
in create_db.sql
where needed. You can check out
Collect Earth Online
to view an example.
A handy use of the build-db
command is to backup and restore your database.
Calling
clojure -M:build-db backup -f somefile.dump
will create a .dump
backup file using pg_dump
.
To restore your database from a .dump
file you will need a .dump
file
containg a copy of a database downloaded locally. Assuming you have a copy of
a database, you can then run:
clojure -M:build-db restore -f somefile.dump
This will copy the database from the .dump
file into your local Postgres
database of the same name as the one in the .dump
file. Note that you will be
prompted with a password after running this command. You should enter the
Postgres master password that you first created when running Postgres after
installing. Depending on the size of your .dump
file, this command may take a
couple of minutes. Note that if you are working on a development branch and your
.dump
file contains a copy of a production database you may also need to apply
some of the SQL changes from the ./sql/changes
directory. Assuming your
database doesn’t have any of the change files on development applied to it,
you can apply all of them at once using the following command:
for filename in ./src/sql/changes/*.sql; do psql -U <db-name> -f $filename; done
triangulum.build-db can also be configured through config.edn. It uses the same configuration as triangulum.database (see above).
To make organizing an application’s configurations simpler, create a
config.edn
file in the project’s root directory. The file is just a hashmap that is similar to:
;; config.edn
{:database {:host "localhost"
:port 5432
:dbname "dbname"
:user "user"
:password "super-secret-password"}
:mail {:host "smtp.gmail.com"
:user "[email protected]"
:pass "3492734923742"
:port 587}
:server {:host "smtp.gmail.com"
:user ""
:pass ""
:tls true
:port 587
:base-url "https://my.domain/"
:auto-validate? false}
...}
You can find an up-to-date example in config.nested-example.edn
file. It can be used as a configuration template for your project.
Add config.edn to your .gitignore
file to keep sensitive information out of
the git history.
To validate the config.edn file, run:
clojure -M:config validate [-f FILE]
To retrieve a configuration, use get-config
. You can supply nested
configuration keys as follows:
(triangulum.config/get-config :database) ;; -> {:user "triangulum" :pass "..."}
(triangulum.config/get-config :database :user) ;; -> "triangulum"
(triangulum.config/get-config :server) ;; -> {:http-port 8080 :mode "dev"}
(triangulum.config/get-config :server :http-port) ;; -> 8080
See each section below for an example configuration if one is required for use.
If you have not already created a SSL certificate, you must start a server
without a https port specified. (e.g. clojure -M:run-server
).
Add the following alias to your deps.edn
file:
{:aliases {:https {:main-opts ["-m" "triangulum.https"]}}}
To automatically create an SSL certificate signed by Let’s Encrypt, simply run the following command from your shell:
sudo clojure -M:https certbot-init -d mydomain.com [-p certbot-dir] [--cert-only]
The certbot creation process will run automatically and silently.
Note: If your certbot installation stores its config files in a directory other than /etc/letsencrypt, you should specify it with the optional certbot-dir argument to certbot-init.
Certbot runs as a background task every 12 hours and will renew any
certificate that is set to expire in 30 days or less. Each time the
certificate is renewed, any script in /etc/letsencrypt/renewal-hooks/deploy
will be run automatically to repackage the updated certificate into the correct
format.
If certbot runs successfully and –cert-only is not specified, then a shell script
[mydomain].sh will be created in the certbot deploy hooks folder.
This script will run clojure -M:https package-cert
. Scripts in this folder will
run automatically when a new certificate is created.
While there should be no need to do so, if you ever want to perform this repackaging step manually, simply run this command from your shell:
sudo clojure -M:https package-cert -d mydomain.com [-p certbot-dir]
Create a shell script in /etc/letsencrypt/renewal-hooks/deploy
and update permissions.
sudo nano /etc/letsencrypt/renewal-hooks/deploy/custom.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/custom.sh
To build a library JAR from your repository, run:
clojure -X triangulum.packaging/build-jar :lib-name $GROUP_ID/$ARTIFACT_ID
To build an application UberJAR from your repository, run:
clojure -X triangulum.packaging/build-uberjar :app-name $ARTIFACT_ID :main-ns $MAIN_NAMESPACE
To deploy a library JAR to https://clojars.org, run:
env CLOJARS_USERNAME=$YOUR_USERNAME CLOJARS_PASSWORD=$YOUR_CLOJARS_TOKEN clojure -X triangulum.packaging/deploy-jar $GROUP_ID/$ARTIFACT_ID
NOTE: As of 2020-06-27, Clojars will no longer accept your Clojars password when deploying. You will have to use a token instead. Please read more about this here
To clean up after yourself by deleting the build folder (target
), run:
clojure -X triangulum.packaging/clean
To make sure your application starts up on system reboot, you can use
Triangulum to create a systemd user .service
file by adding the following to
your :aliases
section in the deps.edn
file:
{:aliases {:systemd {:main-opts ["-m" "triangulum.systemd"]}}}
Modify your app code to call (triangulum.notify/ready!)
after all of your
application’s services are started:
(ns <app>.server
(:require [triangulum.notify :as notify]))
...
(defn app-start []
(reset! db (jdbc/connect!))
(reset! queues (q/start!))
(reset! server (ring/start-server!)
(when (notify/available?) (notify/ready!))))
And then run:
clojure -M:systemd enable -r $REPO [-p $HTTP_PORT] [-P $HTTPS_PORT] [-d $REPO_DIRECTORY] [-A $EXTRA_ALIASES]
This will install a file named cljweb-<repo>.service
into the
~~/.config/systemd/user/~ directory, reload the systemctl
daemon,
and enable your service. By default, the current directory will be
used in the service as the working directory. To supply an
alternative, you can use -d
. This will look for a Clojure project in
that directory.
The server will always be started using clojure -M:server start
unless the --extra-aliases
option is passed. In that case, it will
run with clojure -M${EXTRA_ALIASES}:server start
.
To enable your user services to start on system reboot, you will need to run:
sudo loginctl enable-linger "$USER"
Now your service will be enabled at startup. You can also start, stop, and restart your service with the following commands:
clojure -M:systemd start -r <REPO>
clojure -M:systemd stop -r <REPO>
clojure -M:systemd restart -r <REPO>
See API.md
To generate docs, use: bb docs
Copyright © 2021-2023 Spatial Informatics Group, LLC.
Triangulum is distributed by Spatial Informatics Group, LLC. under the terms of the Eclipse Public License version 2.0 (EPLv2). See LICENSE.txt in this directory for more information.