├── .gitignore ├── test └── co │ └── cljazz │ └── supabase_clj │ └── internals │ ├── http_test.clj │ └── utils_test.clj ├── src └── co │ └── cljazz │ └── supabase_clj │ ├── internals │ ├── utils.clj │ └── http.clj │ └── core.clj ├── LICENSE ├── README.md └── deps.edn /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.pom.asc 4 | *.jar 5 | *.class 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | .nrepl-port 14 | .cpcache/ 15 | *.iml 16 | *.log 17 | 18 | ## application 19 | node_modules 20 | .shadow-cljs 21 | .lsp 22 | .lsp/.cache 23 | .clj-kondo/.cache 24 | .calva 25 | .idea 26 | .lein-* 27 | .nrepl-* 28 | .socket-* 29 | 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /test/co/cljazz/supabase_clj/internals/http_test.clj: -------------------------------------------------------------------------------- 1 | (ns co.cljazz.supabase-clj.internals.http-test 2 | (:require 3 | [co.cljazz.supabase-clj.internals.http :as http] 4 | [clojure.test :refer [deftest is testing]])) 5 | 6 | (deftest req-input->req-map-test 7 | (testing "it should create request map" 8 | (is (= {:body "{\"email\":\"test@te.com\"}", 9 | :content-type :json, 10 | :headers {"apikey" nil} 11 | :redirectTo "http://t.com/auth/t"} 12 | (http/input->req-map 13 | {:body {:email "test@te.com"} 14 | :options {:emailRedirectTo "http://t.com/auth/t"}} 15 | {}))))) 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/co/cljazz/supabase_clj/internals/utils.clj: -------------------------------------------------------------------------------- 1 | (ns co.cljazz.supabase-clj.internals.utils 2 | (:require 3 | [clojure.string :as str])) 4 | 5 | (defn assoc-some [m & kvs] 6 | (reduce (fn [acc [k v]] 7 | (if (some? v) 8 | (assoc acc k v) 9 | acc)) 10 | m 11 | (partition 2 kvs))) 12 | 13 | (defn convert-key [key] 14 | (str/replace key #"_+" "-")) 15 | 16 | (defn extract-parameters [url] 17 | (when (not= (.indexOf url "#") -1) 18 | (let [ params-str (subs url (inc (.indexOf url "#"))) 19 | params (str/split params-str #"&")] 20 | (reduce (fn [result param] 21 | (let [[raw-key value] (str/split param #"=") 22 | key (convert-key raw-key)] 23 | (assoc result (keyword key) value))) 24 | {} 25 | params)))) 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cljazz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # supabase-clj 2 | 3 | 4 | [![Clojars Project](https://img.shields.io/clojars/v/co.cljazz/supabase-clj.svg)](https://clojars.org/co.cljazz/supabase-clj) 5 | 6 | Supabase client with support **clj/cljs** 7 | 8 | ## Discalmer 9 | 10 | > This is a version we are using in an internal code, so 11 | > it my change some contract or way of use it 12 | > 13 | > We have gotrue for now 14 | 15 | ## Install 16 | 17 | 18 | ```edn 19 | co.cljazz/supabase-clj {:mvn/version "0.0.1"} 20 | ``` 21 | 22 | ## Issues 23 | 24 | https://github.com/cljazz/supabase-clj/issues 25 | 26 | 27 | ## References 28 | 29 | https://github.com/supabase/gotrue 30 | 31 | https://github.com/supabase/gotrue-js 32 | 33 | ## Build 34 | 35 | To generate the `.jar` we have the alias `uberjar`: 36 | 37 | ``` 38 | clojure -M:uberjar 39 | ``` 40 | 41 | ### Deploy 42 | 43 | We use **clojars** to host our `.jar`. 44 | 45 | To deploy to clojars it is necessary to generate the "pom" and export the environment variables with clojars *login/password* (tokens), then run the alias `deploy-clojars`: 46 | 47 | ``` 48 | clojure -X:deps mvn-pom 49 | env CLOJARS_USERNAME=username CLOJARS_PASSWORD=clojars-token 50 | clojure -X:deploy-clojars 51 | ``` 52 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"} 3 | org.clojure/data.json {:mvn/version "2.4.0"} 4 | clj-http/clj-http {:mvn/version "3.12.3"}} 5 | 6 | :aliases {:test {:extra-paths ["test"]} 7 | ;; clj -M:clojure-lsp 8 | :clojure-lsp {:replace-deps {com.github.clojure-lsp/clojure-lsp-standalone {:mvn/version "2022.09.01-15.27.31"}} 9 | :main-opts ["-m" "clojure-lsp.main"]} 10 | :nrepl {:extra-deps {cider/cider-nrepl {:mvn/version "0.30.0"}} 11 | :main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]} 12 | ;; clj -M:uberjar 13 | :uberjar {:replace-deps {uberdeps/uberdeps {:mvn/version "1.3.0"}} 14 | :replace-paths [] 15 | :main-opts ["-m" "uberdeps.uberjar" 16 | "--target" "target/supabase-clj.jar"]} 17 | ;; clj -X:deps mvn-pom 18 | ;; env CLOJARS_USERNAME=username CLOJARS_PASSWORD=clojars-token 19 | ;; clj -X:deploy-clojars 20 | :deploy-clojars {:extra-deps {slipset/deps-deploy {:mvn/version "RELEASE"}} 21 | :exec-fn deps-deploy.deps-deploy/deploy 22 | :exec-args {:installer :remote 23 | :sign-releases? true 24 | :artifact "target/supabase-clj.jar"}}}} 25 | -------------------------------------------------------------------------------- /test/co/cljazz/supabase_clj/internals/utils_test.clj: -------------------------------------------------------------------------------- 1 | (ns co.cljazz.supabase-clj.internals.utils-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [co.cljazz.supabase-clj.internals.utils :as utils])) 5 | 6 | (deftest assoc-if-test 7 | (testing "assoc if should work" 8 | (is (= {:a 1 :b 2} 9 | (utils/assoc-some {} :a 1 :b 2))) 10 | (is (= {:a 1} 11 | (utils/assoc-some {} :a 1 :b nil))) 12 | (is (= {:a 1 :b false} 13 | (utils/assoc-some {} :a 1 :b false))) 14 | (is (= {:a 1 :b ""} 15 | (utils/assoc-some {} :a 1 :b ""))))) 16 | 17 | (deftest get-params-from-url-test 18 | (testing "get params from url should work" 19 | (is (= {:refresh-token "kgyByE7FSwxW5r2tUjzS0w" 20 | :token-type "bearer" 21 | :expires-in "3600" 22 | :type "magiclink" 23 | :access-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhRoZW50aWNhdGVkIiwiZXhwIj4ZWU4ZjI2LTRlNjctNDA0GNhOTAxNyb2xlIjoiYXV0aGVudGljYXRlZCIsImFhbCI6ImFhbDEiLCJhbXIiOlt7Im1ldGhvZCI6Im90cCIsInRpbWVzdGFtcCI6MTY4Nzk1M.E47xqL4Z8y9bj91FqOgZ5zzhNz4cY4KWharn1QfYTPQ"} 24 | (utils/extract-parameters 25 | "http://localhost:3000/api/auth/login#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhRoZW50aWNhdGVkIiwiZXhwIj4ZWU4ZjI2LTRlNjctNDA0GNhOTAxNyb2xlIjoiYXV0aGVudGljYXRlZCIsImFhbCI6ImFhbDEiLCJhbXIiOlt7Im1ldGhvZCI6Im90cCIsInRpbWVzdGFtcCI6MTY4Nzk1M.E47xqL4Z8y9bj91FqOgZ5zzhNz4cY4KWharn1QfYTPQ&expires_in=3600&refresh_token=kgyByE7FSwxW5r2tUjzS0w&token_type=bearer&type=magiclink"))) 26 | (is (= {:a "1" :b "2"} 27 | (utils/extract-parameters 28 | "http://t.com/#a=1&b=2"))) 29 | (is (= nil 30 | (utils/extract-parameters "http://t.com"))) 31 | (is (= {:t nil} 32 | (utils/extract-parameters "http://t.com#t"))))) 33 | -------------------------------------------------------------------------------- /src/co/cljazz/supabase_clj/internals/http.clj: -------------------------------------------------------------------------------- 1 | (ns co.cljazz.supabase-clj.internals.http 2 | (:require 3 | [clj-http.client :as client] 4 | [clojure.data.json :as json] 5 | [clojure.pprint :as pprint] 6 | [clojure.string :as str] 7 | [co.cljazz.supabase-clj.internals.utils :as utils])) 8 | 9 | (add-tap (bound-fn* clojure.pprint/pprint)) 10 | 11 | (defn ^:private json->clj-keys 12 | [orig-key] 13 | (-> orig-key 14 | (str/replace #"_" "-") 15 | keyword)) 16 | 17 | (defn read-json [payload] 18 | (json/read-str payload {:key-fn json->clj-keys})) 19 | 20 | (defn ^:private clj->json-keys 21 | [orig-key] 22 | (-> orig-key 23 | name 24 | (str/replace #"-" "_"))) 25 | 26 | (defn write-json [payload] 27 | (json/write-str payload {:key-fn clj->json-keys})) 28 | 29 | (defn ^:private with-body 30 | [req-map {:keys [body]}] 31 | (if body 32 | (merge req-map 33 | {:body (write-json body) 34 | :content-type :json}) 35 | req-map)) 36 | 37 | (defn ^:private with-options 38 | [req-map {:keys [options]}] 39 | (if options 40 | (utils/assoc-some 41 | req-map 42 | :redirect-to (:email-redirect-to options)) 43 | req-map)) 44 | 45 | (defn config+path->url [config path] 46 | (str (:base-url config) path)) 47 | 48 | (defn ^:private config->headers 49 | [{:keys [api-key token]}] 50 | (if token 51 | {"apikey" api-key 52 | "Authorization" (str "Bearer " token)} 53 | {"apikey" api-key})) 54 | 55 | (defn ^:private with-headers 56 | [req-map config] 57 | (merge req-map {:headers (config->headers config)})) 58 | 59 | (defn ^:private as-api-response [http-response options] 60 | (let [body (:body http-response)] 61 | body)) 62 | 63 | (defn input->req-map [params config] 64 | (-> {} 65 | (with-headers config) 66 | (with-body params) 67 | (with-options params))) 68 | 69 | (defn redirect-to [req-map url] 70 | (if (:redirect-to req-map) 71 | (str url "?redirect_to=" (:redirect-to req-map)) 72 | url)) 73 | 74 | (defn post! [path params config] 75 | (let [url (config+path->url config path) 76 | req-map (input->req-map params config) 77 | url (redirect-to req-map url) 78 | response (client/post url req-map)] 79 | (as-api-response response {}))) 80 | 81 | (defn get! 82 | [path config & {:as options}] 83 | (let [url (config+path->url config path) 84 | req-map (-> {} 85 | (with-headers config)) 86 | response (client/get url req-map) 87 | options (merge {:parse? true} options)] 88 | (as-api-response response options))) 89 | -------------------------------------------------------------------------------- /src/co/cljazz/supabase_clj/core.clj: -------------------------------------------------------------------------------- 1 | (ns co.cljazz.supabase-clj.core 2 | (:require 3 | [co.cljazz.supabase-clj.internals.http :refer [get! post!] :as internal.http] 4 | [co.cljazz.supabase-clj.internals.utils :as utils])) 5 | 6 | (defn signin-with-email 7 | " 8 | Disclamer! this function is a opinionated 9 | Send a user a passwordless link which they can use to redeem an access_token. 10 | After they have clicked the link,client will redirect for your config callback link. 11 | 12 | (signin-with-email 13 | {:base-url '' 14 | :email '' 15 | :should-create-user false ; default is true 16 | :api-key '' 17 | :options {:email-redirect-to ''}}) 18 | 19 | email-redirect-to: is default from your supabase config on their website 20 | api-key: your api key from supabase 21 | base-url: your supabase url 22 | " 23 | [{:keys [base-url 24 | email 25 | api-key 26 | should-create-user 27 | options]}] 28 | (let [params {:body (utils/assoc-some 29 | {:email email :create-user true} 30 | :create-user should-create-user) 31 | :options options} 32 | config {:api-key api-key 33 | :base-url (str base-url "/auth/v1")}] 34 | (post! "/otp" params config))) 35 | 36 | (defn get-user 37 | " 38 | Get the JSON object for the logged in user. 39 | token: jwt from your redirect url 40 | api-key: your api key from supabase 41 | base-url: your supabase url 42 | 43 | (get-user 44 | {:token '' 45 | :api-key '' 46 | :base-url ''}) 47 | " 48 | [{:keys [base-url] :as config}] 49 | (let [base-url (str base-url "/auth/v1")] 50 | (internal.http/read-json (get! "/user" 51 | (assoc config 52 | :base-url base-url))))) 53 | 54 | (defn session-from-url 55 | " 56 | If an account is created, users can login to your app. 57 | This functions returns an user 58 | 59 | url: your redirect url 60 | api-key: your api key from supabase 61 | base-url: your supabase url 62 | 63 | " 64 | [{:keys [base-url url api-key]}] 65 | (let [{:keys [refresh-token]} (utils/extract-parameters url) 66 | base-url (str base-url "/auth/v1")] 67 | (internal.http/read-json 68 | (post! "/token?grant_type=refresh_token" {:body {:refresh-token refresh-token}} 69 | {:api-key api-key 70 | :base-url base-url})))) 71 | 72 | (defn refresh-session 73 | " 74 | Generates a new JWT. 75 | @param refresh-token A valid refresh token that was returned on login. 76 | 77 | api-key: your api key from supabase 78 | base-url: your supabase url 79 | " 80 | [{:keys [base-url 81 | api-key 82 | refresh-token]}] 83 | (let [base-url (str base-url "/auth/v1")] 84 | (internal.http/read-json 85 | (post! "/token?grant_type=refresh_token" {:body {:refresh-token refresh-token}} 86 | {:base-url base-url 87 | :api-key api-key})))) 88 | 89 | --------------------------------------------------------------------------------