Skip to content

sig-gis/triangulum

Repository files navigation

Triangulum

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

Adding Triangulum to your Project

  1. Add the library’s coordinates to the :deps map in your project’s deps.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"}}})
        
  2. Define one or more aliases for Triangulum’s utility scripts in the :aliases map in your project’s deps.edn file.

    NOTE: You can find examples of these aliases in the deps.example.edn file in the root directory of this repository.

  3. 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 and config.namespaced-example.edn within this repository.

Running Tests

To launch the test suite, run:

clojure -M:test

Web Framework

triangulum.server

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.

  1. 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
               :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"}
        
  2. 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 the wrap-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.

triangulum.handler

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.

Usage

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:

  1. Set :handler to triangulum.handler/authenticated-routing-handler.
;; nested config
{:server {:handler triangulum.handler/authenticated-routing-handler}}

;; namespaced config
{:triangulum.server/handler triangulum.handler/authenticated-routing-handler}
  1. 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 your config.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.

  1. triangulum.handler/authenticated-routing-handler will use any custom implementation of these handlers that you specify in your config.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}

Functions

triangulum.views

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.

Usage

  1. Require the namespace in your project.
  2. Use render-page to generate a handler that will return an HTML response with a standard template for React/Reagent web apps.

Example

(ns my-app.views
  (:require [triangulum.views :refer [render-page]]))

(def my-page-handler (render-page "/my-page"))

Functions

render-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”))

Caveat

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" }}}

triangulum.git

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.

Utility Scripts

triangulum.build-db

Required Prerequisites

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).

triangulum.config

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           "test@example.com"
            :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.

triangulum.https

Required Prerequisites

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.

Default Renewal Hook

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]

Custom Renewal Hook

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

triangulum.packaging

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

triangulum.systemd

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>

API

See API.md

Docs

To generate docs, use: bb docs

License

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.

About

A set of plugins that loosely form a framework for deploying clojure web apps with jetty and ring.

Resources

License

Stars

Watchers

Forks

Packages

No packages published