♻️ Refactor backend.

Move from custom vertx to jetty9.
This commit is contained in:
Andrey Antukh 2020-05-14 13:49:11 +02:00 committed by Alonso Torres
parent 1639e15975
commit 5a03c13731
82 changed files with 1763 additions and 4667 deletions

View file

@ -5,11 +5,13 @@
:deps :deps
{org.clojure/clojure {:mvn/version "1.10.1"} {org.clojure/clojure {:mvn/version "1.10.1"}
org.clojure/data.json {:mvn/version "1.0.0"} org.clojure/data.json {:mvn/version "1.0.0"}
org.clojure/core.async {:mvn/version "1.2.603"}
;; Logging ;; Logging
org.clojure/tools.logging {:mvn/version "1.1.0"} org.clojure/tools.logging {:mvn/version "1.1.0"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.13.2"} org.apache.logging.log4j/log4j-api {:mvn/version "2.13.2"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.13.2"} org.apache.logging.log4j/log4j-core {:mvn/version "2.13.2"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.13.2"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.13.2"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.13.2"}
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.13.2"} org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.13.2"}
@ -17,14 +19,14 @@
instaparse/instaparse {:mvn/version "1.4.10"} instaparse/instaparse {:mvn/version "1.4.10"}
com.cognitect/transit-clj {:mvn/version "1.0.324"} com.cognitect/transit-clj {:mvn/version "1.0.324"}
;; TODO: vendorize pgclient under `vertx-clojure/vertx-pgclient`
io.vertx/vertx-pg-client {:mvn/version "4.0.0-milestone4"}
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"} io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.1"} java-http-clj/java-http-clj {:mvn/version "0.4.1"}
vertx-clojure/vertx info.sunng/ring-jetty9-adapter {:mvn/version "0.12.8"}
{:local/root "vendor/vertx" seancorfield/next.jdbc {:mvn/version "1.0.424"}
:deps/manifest :pom} metosin/reitit-ring {:mvn/version "0.4.2"}
org.postgresql/postgresql {:mvn/version "42.2.12"}
com.zaxxer/HikariCP {:mvn/version "3.4.3"}
funcool/datoteka {:mvn/version "1.2.0"} funcool/datoteka {:mvn/version "1.2.0"}
funcool/promesa {:mvn/version "5.1.0"} funcool/promesa {:mvn/version "5.1.0"}

View file

@ -1,18 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.core
(:require
[vertx.core :as vc]
[vertx.timers :as vt]
[mount.core :as mount :refer [defstate]]))
(defstate system
:start (vc/system)
:stop (vc/stop system))

View file

@ -6,49 +6,109 @@
(ns uxbox.db (ns uxbox.db
(:require (:require
[clojure.string :as str]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[lambdaisland.uri :refer [uri]] [lambdaisland.uri :refer [uri]]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[promesa.core :as p] [next.jdbc :as jdbc]
[next.jdbc.date-time :as jdbc-dt]
[next.jdbc.optional :as jdbc-opt]
[next.jdbc.result-set :as jdbc-rs]
[next.jdbc.sql :as jdbc-sql]
[next.jdbc.sql.builder :as jdbc-bld]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.core :refer [system]] [uxbox.util.data :as data])
[uxbox.util.data :as data] (:import
[uxbox.util.pgsql :as pg] com.zaxxer.hikari.HikariConfig
[vertx.core :as vx])) com.zaxxer.hikari.HikariDataSource))
(defn- create-datasource-config
[cfg]
(let [dburi (:database-uri cfg)
username (:database-username cfg)
password (:database-password cfg)
config (HikariConfig.)]
(doto config
(.setJdbcUrl (str "jdbc:" dburi))
(.setAutoCommit true)
(.setReadOnly false)
(.setConnectionTimeout 30000)
(.setValidationTimeout 5000)
(.setIdleTimeout 600000)
(.setMaxLifetime 1800000)
(.setMinimumIdle 10)
(.setMaximumPoolSize 20))
(when username (.setUsername config username))
(when password (.setPassword config password))
config))
(defn- create-pool (defn- create-pool
[config system] [cfg]
(let [dburi (:database-uri config) (let [dsc (create-datasource-config cfg)]
username (:database-username config) (jdbc-dt/read-as-instant)
password (:database-password config) (HikariDataSource. dsc)))
dburi (-> (uri dburi)
(assoc :user username)
(assoc :password password)
(str))]
(log/info "creating connection pool with" dburi)
(pg/pool dburi {:system system :max-size 8})))
(defstate pool (defstate pool
:start (create-pool cfg/config system)) :start (create-pool cfg/config)
:stop (.close pool))
(defmacro with-atomic (defmacro with-atomic
[bindings & args] [& args]
`(pg/with-atomic ~bindings (p/do! ~@args))) `(jdbc/with-transaction ~@args))
(def row-xfm (defn- kebab-case [s] (str/replace s #"_" "-"))
(comp (map pg/row->map) (defn- snake-case [s] (str/replace s #"-" "_"))
(map data/normalize-attrs))) (defn- as-kebab-maps
[rs opts]
(jdbc-opt/as-unqualified-modified-maps rs (assoc opts :label-fn kebab-case)))
(defmacro query (defn open
[conn sql] []
`(-> (pg/query ~conn ~sql {:xfm row-xfm}) (jdbc/get-connection pool))
(p/catch' (fn [err#]
(ex/raise :type :database-error (defn exec!
:cause err#))))) [ds sv]
(defmacro query-one (jdbc/execute! ds sv {:builder-fn as-kebab-maps}))
[conn sql]
`(-> (pg/query-one ~conn ~sql {:xfm row-xfm}) (defn exec-one!
(p/catch' (fn [err#] ([ds sv] (exec-one! ds sv {}))
(ex/raise :type :database-error ([ds sv opts]
:cause err#))))) (jdbc/execute-one! ds sv (assoc opts :builder-fn as-kebab-maps))))
(def ^:private default-options
{:table-fn snake-case
:column-fn snake-case
:builder-fn as-kebab-maps})
(defn insert!
[ds table params]
(jdbc-sql/insert! ds table params default-options))
(defn update!
[ds table params where]
(let [opts (assoc default-options :return-keys true)]
(jdbc-sql/update! ds table params where opts)))
(defn delete!
[ds table params]
(let [opts (assoc default-options :return-keys true)]
(jdbc-sql/delete! ds table params opts)))
(defn get-by-params
([ds table params]
(get-by-params ds table params nil))
([ds table params opts]
(let [opts (cond-> (merge default-options opts)
(:for-update opts)
(assoc :suffix "for update"))
res (exec-one! ds (jdbc-bld/for-query table params opts) opts)]
(when (:deleted-at res)
(ex/raise :type :not-found))
res)))
(defn get-by-id
([ds table id]
(get-by-params ds table {:id id} nil))
([ds table id opts]
(get-by-params ds table {:id id} opts)))

View file

@ -27,8 +27,8 @@
(defn render (defn render
[email context] [email context]
(let [defaults {:from (:email-from cfg/config) (let [defaults {:from (:sendmail-from cfg/config)
:reply-to (:email-reply-to cfg/config)}] :reply-to (:sendmail-reply-to cfg/config)}]
(email (merge defaults context)))) (email (merge defaults context))))
(defn send! (defn send!

View file

@ -8,20 +8,17 @@
"A initial fixtures." "A initial fixtures."
(:require (:require
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[sodi.pwhash :as pwhash]
[mount.core :as mount] [mount.core :as mount]
[promesa.core :as p] [sodi.pwhash :as pwhash]
[uxbox.config :as cfg]
[uxbox.common.pages :as cp]
[uxbox.common.data :as d] [uxbox.common.data :as d]
[uxbox.core] [uxbox.common.pages :as cp]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.migrations] [uxbox.migrations]
[uxbox.services.mutations.profile :as mt.profile] [uxbox.services.mutations.profile :as mt.profile]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]))
[uxbox.common.uuid :as uuid]
[vertx.util :as vu]))
(defn- mk-uuid (defn- mk-uuid
[prefix & args] [prefix & args]
@ -31,52 +28,6 @@
(def password (pwhash/derive "123123")) (def password (pwhash/derive "123123"))
(def sql:create-team
"insert into team (id, name, photo)
values ($1, $2, $3)
returning *;")
(def sql:create-team-profile
"insert into team_profile_rel (team_id, profile_id, is_owner, is_admin, can_edit)
values ($1, $2, $3, $4, $5)
returning *;")
(def sql:create-project
"insert into project (id, team_id, name)
values ($1, $2, $3)
returning *;")
(def sql:create-project-profile
"insert into project_profile_rel (project_id, profile_id, is_owner, is_admin, can_edit)
values ($1, $2, $3, $4, $5)
returning *")
(def sql:create-file-profile
"insert into file_profile_rel (file_id, profile_id, is_owner, is_admin, can_edit)
values ($1, $2, $3, $4, $5)
returning *")
(def sql:create-file
"insert into file (id, project_id, name)
values ($1, $2, $3 ) returning *")
(def sql:create-page
"insert into page (id, file_id, name,
version, ordering, data)
values ($1, $2, $3, $4, $5, $6)
returning id;")
(def sql:create-icon-library
"insert into icon_library (team_id, name)
values ($1, $2)
returning id;")
(def sql:create-icon
"insert into icon_library (library_id, name, content, metadata)
values ($1, $2, $3, $4)
returning id;")
(def preset-small (def preset-small
{:num-teams 5 {:num-teams 5
:num-profiles 5 :num-profiles 5
@ -113,13 +64,7 @@
(defn- collect (defn- collect
[f items] [f items]
(reduce (fn [acc n] (reduce #(conj %1 (f %2)) [] items))
(p/then acc (fn [acc]
(p/then (f n)
(fn [res]
(conj acc res))))))
(p/promise [])
items))
(defn impl-run (defn impl-run
[opts] [opts]
@ -144,13 +89,13 @@
create-team create-team
(fn [conn index] (fn [conn index]
(let [sql sql:create-team (let [id (mk-uuid "team" index)
id (mk-uuid "team" index)
name (str "Team" index)] name (str "Team" index)]
(log/info "create team" id) (log/info "create team" id)
(db/insert! conn :team {:id id
(-> (db/query-one conn [sql id name ""]) :name name
(p/then (constantly id))))) :photo ""})
id))
create-teams create-teams
(fn [conn] (fn [conn]
@ -160,114 +105,143 @@
create-page create-page
(fn [conn owner-id project-id file-id index] (fn [conn owner-id project-id file-id index]
(p/let [id (mk-uuid "page" project-id file-id index) (let [id (mk-uuid "page" project-id file-id index)
data cp/default-page-data data cp/default-page-data
name (str "page " index) name (str "page " index)
version 0 version 0
ordering index ordering index
data (blob/encode data)] data (blob/encode data)]
(log/info "create page" id) (log/info "create page" id)
(db/query-one conn [sql:create-page (db/insert! conn :page
id file-id name version ordering data]))) {:id id
:file-id file-id
:name name
:version version
:ordering ordering
:data data})))
create-pages create-pages
(fn [conn owner-id project-id file-id] (fn [conn owner-id project-id file-id]
(log/info "create pages") (log/info "create pages")
(p/run! (partial create-page conn owner-id project-id file-id) (run! (partial create-page conn owner-id project-id file-id)
(range (:num-pages-per-file opts)))) (range (:num-pages-per-file opts))))
create-file create-file
(fn [conn owner-id project-id index] (fn [conn owner-id project-id index]
(p/let [id (mk-uuid "file" project-id index) (let [id (mk-uuid "file" project-id index)
name (str "file" index)] name (str "file" index)]
(log/info "create file" id) (log/info "create file" id)
(db/query-one conn [sql:create-file id project-id name]) (db/insert! conn :file
(db/query-one conn [sql:create-file-profile {:id id
id owner-id true true true]) :project-id project-id
:name name})
(db/insert! conn :file-profile-rel
{:file-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id)) id))
create-files create-files
(fn [conn owner-id project-id] (fn [conn owner-id project-id]
(log/info "create files") (log/info "create files")
(p/let [file-ids (collect (partial create-file conn owner-id project-id) (let [file-ids (collect (partial create-file conn owner-id project-id)
(range (:num-files-per-project opts)))] (range (:num-files-per-project opts)))]
(p/run! (partial create-pages conn owner-id project-id) file-ids))) (run! (partial create-pages conn owner-id project-id) file-ids)))
create-project create-project
(fn [conn team-id owner-id index] (fn [conn team-id owner-id index]
(p/let [id (mk-uuid "project" team-id index) (let [id (mk-uuid "project" team-id index)
name (str "project " index)] name (str "project " index)]
(log/info "create project" id) (log/info "create project" id)
(db/query-one conn [sql:create-project id team-id name]) (db/insert! conn :project
(db/query-one conn [sql:create-project-profile {:id id
id owner-id true true true]) :team-id team-id
:name name})
(db/insert! conn :project-profile-rel
{:project-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id)) id))
create-projects create-projects
(fn [conn team-id profile-ids] (fn [conn team-id profile-ids]
(log/info "create projects") (log/info "create projects")
(p/let [owner-id (rng-nth rng profile-ids) (let [owner-id (rng-nth rng profile-ids)
project-ids (collect (partial create-project conn team-id owner-id) project-ids (collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))] (range (:num-projects-per-team opts)))]
(p/run! (partial create-files conn owner-id) project-ids))) (run! (partial create-files conn owner-id) project-ids)))
assign-profile-to-team assign-profile-to-team
(fn [conn team-id owner? profile-id] (fn [conn team-id owner? profile-id]
(let [sql sql:create-team-profile] (db/insert! conn :team-profile-rel
(db/query-one conn [sql team-id profile-id owner? true true]))) {:team-id team-id
:profile-id profile-id
:is-owner owner?
:is-admin true
:can-edit true}))
setup-team setup-team
(fn [conn team-id profile-ids] (fn [conn team-id profile-ids]
(log/info "setup team" team-id profile-ids) (log/info "setup team" team-id profile-ids)
(p/do!
(assign-profile-to-team conn team-id true (first profile-ids)) (assign-profile-to-team conn team-id true (first profile-ids))
(p/run! (partial assign-profile-to-team conn team-id false) (run! (partial assign-profile-to-team conn team-id false)
(rest profile-ids)) (rest profile-ids))
(create-projects conn team-id profile-ids))) (create-projects conn team-id profile-ids))
assign-teams-and-profiles assign-teams-and-profiles
(fn [conn teams profiles] (fn [conn teams profiles]
(log/info "assign teams and profiles") (log/info "assign teams and profiles")
(vu/loop [team-id (first teams) (loop [team-id (first teams)
teams (rest teams)] teams (rest teams)]
(when-not (nil? team-id) (when-not (nil? team-id)
(p/let [n-profiles-team (:num-profiles-per-team opts) (let [n-profiles-team (:num-profiles-per-team opts)
selected-profiles (rng-vec rng profiles n-profiles-team)] selected-profiles (rng-vec rng profiles n-profiles-team)]
(setup-team conn team-id selected-profiles) (setup-team conn team-id selected-profiles)
(p/recur (first teams) (recur (first teams)
(rest teams)))))) (rest teams))))))
create-draft-pages create-draft-pages
(fn [conn owner-id file-id] (fn [conn owner-id file-id]
(log/info "create draft pages") (log/info "create draft pages")
(p/run! (partial create-page conn owner-id nil file-id) (run! (partial create-page conn owner-id nil file-id)
(range (:num-draft-pages-per-file opts)))) (range (:num-draft-pages-per-file opts))))
create-draft-file create-draft-file
(fn [conn owner index] (fn [conn owner index]
(p/let [owner-id (:id owner) (let [owner-id (:id owner)
id (mk-uuid "file" "draft" owner-id index) id (mk-uuid "file" "draft" owner-id index)
name (str "file" index) name (str "file" index)
project-id (:id (:default-project owner))] project-id (:default-project owner)]
(log/info "create draft file" id) (log/info "create draft file" id)
(db/query-one conn [sql:create-file id project-id name]) (db/insert! conn :file
(db/query-one conn [sql:create-file-profile {:id id
id owner-id true true true]) :project-id project-id
:name name})
(db/insert! conn :file-profile-rel
{:file-id id
:profile-id owner-id
:is-owner true
:is-admin true
:can-edit true})
id)) id))
create-draft-files create-draft-files
(fn [conn profile] (fn [conn profile]
(p/let [file-ids (collect (partial create-draft-file conn profile) (let [file-ids (collect (partial create-draft-file conn profile)
(range (:num-draft-files-per-profile opts)))] (range (:num-draft-files-per-profile opts)))]
(p/run! (partial create-draft-pages conn (:id profile)) file-ids))) (run! (partial create-draft-pages conn (:id profile)) file-ids)))
] ]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [profiles (create-profiles conn) (let [profiles (create-profiles conn)
teams (create-teams conn)] teams (create-teams conn)]
(assign-teams-and-profiles conn teams (map :id profiles)) (assign-teams-and-profiles conn teams (map :id profiles))
(p/run! (partial create-draft-files conn) profiles))))) (run! (partial create-draft-files conn) profiles)))))
(defn run (defn run
[preset] [preset]
@ -278,17 +252,15 @@
;; "medium" preset-medium ;; "medium" preset-medium
;; "big" preset-big ;; "big" preset-big
preset-small))] preset-small))]
(deref (impl-run preset)))) (impl-run preset)))
(defn -main (defn -main
[& args] [& args]
(try (try
(-> (mount/only #{#'uxbox.config/config (-> (mount/only #{#'uxbox.config/config
#'uxbox.core/system
#'uxbox.db/pool #'uxbox.db/pool
#'uxbox.migrations/migrations}) #'uxbox.migrations/migrations})
(mount/start)) (mount/start))
(run (first args)) (run (first args))
(finally (finally
(mount/stop)))) (mount/stop))))

View file

@ -8,70 +8,57 @@
(:require (:require
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[promesa.core :as p] [reitit.ring :as rring]
[uxbox.core :refer [system]] [ring.adapter.jetty9 :as jetty]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.http.debug :as debug]
[uxbox.http.errors :as errors] [uxbox.http.errors :as errors]
[uxbox.http.handlers :as handlers]
[uxbox.http.middleware :as middleware] [uxbox.http.middleware :as middleware]
[uxbox.http.session :as session] [uxbox.http.session :as session]
[uxbox.http.handlers :as handlers]
[uxbox.http.debug :as debug]
[uxbox.http.ws :as ws] [uxbox.http.ws :as ws]
[vertx.core :as vc] [uxbox.services.notifications :as usn]))
[vertx.http :as vh]
[vertx.web :as vw]
[vertx.web.middleware :as vwm]))
(defn- on-start (defn- create-router
[ctx] []
(let [cors-opts {:origin (:http-server-cors cfg/config "http://localhost:3449") (rring/router
:max-age 3600 [["/api" {:middleware [[middleware/format-response-body]
:allow-credentials true [middleware/errors errors/handle]
:allow-methods #{:post :get :patch :head :options :put}
:allow-headers #{:x-requested-with :content-type :cookie}}
routes [["/notifications/:file-id/:session-id"
{:middleware [[vwm/cookies]
[vwm/cors cors-opts]
[middleware/format-response-body]
[session/auth]]
:handler ws/handler
:method :get}]
["/api" {:middleware [[vwm/cookies]
[vwm/params]
[vwm/cors cors-opts]
[middleware/parse-request-body] [middleware/parse-request-body]
[middleware/format-response-body] [middleware/params]
[middleware/method-match] [middleware/multipart-params]
[vwm/errors errors/handle]]} [middleware/keyword-params]
["/echo" {:handler handlers/echo-handler}] [middleware/cookies]]}
["/echo" {:get handlers/echo-handler
:post handlers/echo-handler}]
["/login" {:handler handlers/login-handler ["/login" {:handler handlers/login-handler
:method :post}] :method :post}]
["/logout" {:handler handlers/logout-handler ["/logout" {:handler handlers/logout-handler
:method :post}] :method :post}]
["/w" {:middleware [session/auth]} ["/w" {:middleware [session/auth]}
["/mutation/:type" {:middleware [vwm/uploads] ["/query/:type" {:get handlers/query-handler}]
:handler handlers/mutation-handler ["/mutation/:type" {:post handlers/mutation-handler}]]]]))
:method :post}]
["/query/:type" {:handler handlers/query-handler
:method :get}]]]]
handler (vw/handler ctx (defstate app
(vw/assets "/media/*" {:root "resources/public/media"}) :start (rring/ring-handler
(vw/assets "/static/*" {:root "resources/public/static"}) (create-router)
(vw/router routes))] (constantly {:status 404, :body ""})
{:middleware [middleware/development-resources
middleware/development-cors]}))
(log/info "Starting http server on" (:http-server-port cfg/config) "port.") (defn start-server
(vh/server ctx {:handler handler [cfg app]
:port (:http-server-port cfg/config)}))) (let [wsockets {"/ws/notifications" ws/handler}
options {:port (:http-server-port cfg)
(def num-cpus :h2c? true
(delay (.availableProcessors (Runtime/getRuntime)))) :join? false
:allow-null-path-info true
:websockets wsockets}]
(jetty/run-jetty app options)))
(defstate server (defstate server
:start (let [vf (vc/verticle {:on-start on-start})] :start (start-server cfg/config app)
@(vc/deploy! system vf {:instances @num-cpus}))) :stop (.stop server))

View file

@ -6,16 +6,13 @@
(ns uxbox.http.handlers (ns uxbox.http.handlers
(:require (:require
[promesa.core :as p]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.uuid :as uuid]
[uxbox.emails :as emails] [uxbox.emails :as emails]
[uxbox.http.session :as session] [uxbox.http.session :as session]
[uxbox.services.init] [uxbox.services.init]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]))
[uxbox.common.uuid :as uuid]
[vertx.web :as vw]
[vertx.eventbus :as ve]))
(def unauthorized-services (def unauthorized-services
#{:create-demo-profile #{:create-demo-profile
@ -36,10 +33,8 @@
(:profile-id req) (assoc :profile-id (:profile-id req)))] (:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req) (if (or (:profile-id req)
(contains? unauthorized-services type)) (contains? unauthorized-services type))
(-> (sq/handle (with-meta data {:req req}))
(p/then' (fn [result]
{:status 200 {:status 200
:body result}))) :body (sq/handle (with-meta data {:req req}))}
{:status 403 {:status 403
:body {:type :authentication :body {:type :authentication
:code :unauthorized}}))) :code :unauthorized}})))
@ -55,9 +50,8 @@
(:profile-id req) (assoc :profile-id (:profile-id req)))] (:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req) (if (or (:profile-id req)
(contains? unauthorized-services type)) (contains? unauthorized-services type))
(-> (sm/handle (with-meta data {:req req})) {:status 200
(p/then' (fn [result] :body (sm/handle (with-meta data {:req req}))}
{:status 200 :body result})))
{:status 403 {:status 403
:body {:type :authentication :body {:type :authentication
:code :unauthorized}}))) :code :unauthorized}})))
@ -66,7 +60,7 @@
[req] [req]
(let [data (:body-params req) (let [data (:body-params req)
user-agent (get-in req [:headers "user-agent"])] user-agent (get-in req [:headers "user-agent"])]
(p/let [profile (sm/handle (assoc data ::sm/type :login)) (let [profile (sm/handle (assoc data ::sm/type :login))
token (session/create (:id profile) user-agent)] token (session/create (:id profile) user-agent)]
{:status 200 {:status 200
:cookies {"auth-token" {:value token :path "/"}} :cookies {"auth-token" {:value token :path "/"}}
@ -76,16 +70,15 @@
[req] [req]
(some-> (get-in req [:cookies "auth-token"]) (some-> (get-in req [:cookies "auth-token"])
(uuid/uuid) (uuid/uuid)
(session/delete) (session/delete))
(p/then' (fn [token]
{:status 204 {:status 204
:cookies {"auth-token" nil} :cookies {"auth-token" nil}
:body ""})))) :body ""})
(defn echo-handler (defn echo-handler
[req] [req]
(p/promise {:status 200 {:status 200
:body {:params (:params req) :body {:params (:params req)
:cookies (:cookies req) :cookies (:cookies req)
:headers (:headers req)}})) :headers (:headers req)}})

View file

@ -9,30 +9,32 @@
(ns uxbox.http.middleware (ns uxbox.http.middleware
(:require (:require
[promesa.core :as p] [clojure.tools.logging :as log]
[vertx.web :as vw] [ring.middleware.cookies :refer [wrap-cookies]]
[uxbox.config :as cfg] [ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.resource :refer [wrap-resource]]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.util.transit :as t]) [uxbox.config :as cfg]
(:import [uxbox.util.transit :as t]))
io.vertx.ext.web.RoutingContext
io.vertx.ext.web.FileUpload
io.vertx.core.buffer.Buffer))
(defn- wrap-parse-request-body (defn- wrap-parse-request-body
[handler] [handler]
(fn [{:keys [headers body method] :as request}] (letfn [(parse-body [body]
(let [mtype (get headers "content-type")]
(if (and (= "application/transit+json" mtype)
(not= method :get))
(try (try
(let [params (t/decode (t/buffer->bytes body))] (let [reader (t/reader body)]
(handler (assoc request :body-params params))) (t/read! reader))
(catch Exception e (catch Exception e
(ex/raise :type :parse (ex/raise :type :parse
:message "Unable to parse transit from request body." :message "Unable to parse transit from request body."
:cause e))) :cause e))))]
(handler request))))) (fn [{:keys [headers body request-method] :as request}]
(handler
(cond-> request
(and (= "application/transit+json" (get headers "content-type"))
(not= request-method :get))
(assoc :body-params (parse-body body)))))))
(def parse-request-body (def parse-request-body
{:name ::parse-request-body {:name ::parse-request-body
@ -47,7 +49,7 @@
(cond (cond
(coll? body) (coll? body)
(-> response (-> response
(assoc :body (t/bytes->buffer (t/encode body {:type type}))) (assoc :body (t/encode body {:type type}))
(update :headers assoc (update :headers assoc
"content-type" "content-type"
"application/transit+json")) "application/transit+json"))
@ -61,26 +63,69 @@
(defn- wrap-format-response-body (defn- wrap-format-response-body
[handler] [handler]
(fn [request] (fn [request]
(-> (p/do! (handler request)) (let [response (handler request)]
(p/then' (fn [response]
(cond-> response (cond-> response
(map? response) (impl-format-response-body))))))) (map? response) (impl-format-response-body)))))
(def format-response-body (def format-response-body
{:name ::format-response-body {:name ::format-response-body
:compile (constantly wrap-format-response-body)}) :compile (constantly wrap-format-response-body)})
(defn- wrap-errors
(defn- wrap-method-match [handler on-error]
[handler]
(fn [request]))
(def method-match
{:name ::method-match
:compile (fn [data opts]
(when-let [method (:method data)]
(fn [handler]
(fn [request] (fn [request]
(if (= (:method request) method) (try
(handler request) (handler request)
{:status 405 :body ""})))))}) (catch Throwable e
(on-error e request)))))
(def errors
{:name ::errors
:compile (constantly wrap-errors)})
(def cookies
{:name ::cookies
:compile (constantly wrap-cookies)})
(def params
{:name ::params
:compile (constantly wrap-params)})
(def multipart-params
{:name ::multipart-params
:compile (constantly wrap-multipart-params)})
(def keyword-params
{:name ::keyword-params
:compile (constantly wrap-keyword-params)})
(defn- wrap-development-cors
[handler]
(letfn [(add-cors-headers [response]
(update response :headers
(fn [headers]
(-> headers
(assoc "access-control-allow-origin" "http://localhost:3449")
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "content-type")))))]
(fn [request]
(if (= (:request-method request) :options)
(-> {:status 200 :body ""}
(add-cors-headers))
(let [response (handler request)]
(add-cors-headers response))))))
(def development-cors
{:name ::development-cors
:compile (fn [& args]
(when *assert*
wrap-development-cors))})
(def development-resources
{:name ::development-resources
:compile (fn [& args]
(when *assert*
#(wrap-resource % "public")))})

View file

@ -6,8 +6,6 @@
(ns uxbox.http.session (ns uxbox.http.session
(:require (:require
[promesa.core :as p]
[vertx.core :as vc]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.common.uuid :as uuid])) [uxbox.common.uuid :as uuid]))
@ -17,22 +15,21 @@
"Retrieves a user id associated with the provided auth token." "Retrieves a user id associated with the provided auth token."
[token] [token]
(when token (when token
(let [sql "select profile_id from session where id = $1"] (let [row (db/get-by-params db/pool :session {:id token})]
(-> (db/query-one db/pool [sql token]) (:profile-id row))))
(p/then' (fn [row] (when row (:profile-id row))))))))
(defn create (defn create
[user-id user-agent] [user-id user-agent]
(let [id (uuid/random) (let [id (uuid/random)]
sql "insert into session (id, profile_id, user_agent) values ($1, $2, $3)"] (db/insert! db/pool :session {:id id
(-> (db/query-one db/pool [sql id user-id user-agent]) :profile-id user-id
(p/then (constantly (str id)))))) :user-agent user-agent})
(str id)))
(defn delete (defn delete
[token] [token]
(let [sql "delete from session where id = $1"] (db/delete! db/pool :session {:id token})
(-> (db/query-one db/pool [sql token]) nil)
(p/then' (constantly nil)))))
;; --- Interceptor ;; --- Interceptor
@ -40,19 +37,18 @@
[request] [request]
(try (try
(when-let [token (get-in request [:cookies "auth-token"])] (when-let [token (get-in request [:cookies "auth-token"])]
(uuid/uuid token)) (uuid/uuid (:value token)))
(catch java.lang.IllegalArgumentException e (catch java.lang.IllegalArgumentException e
nil))) nil)))
(defn- wrap-auth (defn wrap-auth
[handler] [handler]
(fn [request] (fn [request]
(let [token (parse-token request)] (let [token (parse-token request)
(-> (p/do! (retrieve token)) profile-id (retrieve token)]
(p/then (fn [profile-id]
(if profile-id (if profile-id
(handler (assoc request :profile-id profile-id)) (handler (assoc request :profile-id profile-id))
(handler request)))))))) (handler request)))))
(def auth (def auth
{:nane ::auth {:nane ::auth

View file

@ -7,12 +7,37 @@
(ns uxbox.http.ws (ns uxbox.http.ws
"Web Socket handlers" "Web Socket handlers"
(:require (:require
[uxbox.services.notifications :as nf] [clojure.core.async :as a]
[vertx.web.websockets :as ws])) [ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.params :refer [wrap-params]]
[uxbox.http.session :refer [wrap-auth]]
[clojure.tools.logging :as log]
[clojure.spec.alpha :as s]
[promesa.core :as p]
[ring.adapter.jetty9 :as jetty]
[uxbox.common.exceptions :as ex]
[uxbox.common.uuid :as uuid]
[uxbox.common.spec :as us]
[uxbox.redis :as redis]
[ring.util.codec :as codec]
[uxbox.util.transit :as t]
[uxbox.services.notifications :as nf]))
(defn handler (s/def ::file-id ::us/uuid)
[{:keys [user] :as req}] (s/def ::session-id ::us/uuid)
(ws/websocket (s/def ::websocket-params
{:handler #(nf/websocket req %) (s/keys :req-un [::file-id ::session-id]))
:input-buffer-size 64
:output-buffer-size 64})) (defn websocket
[req]
(let [params (us/conform ::websocket-params (:params req))
params (assoc params :profile-id (:profile-id req))]
(nf/websocket params)))
(def handler
(-> websocket
(wrap-auth)
(wrap-keyword-params)
(wrap-cookies)
(wrap-params)))

View file

@ -15,7 +15,6 @@
[clojure.pprint :refer [pprint]] [clojure.pprint :refer [pprint]]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.edn :as edn] [clojure.edn :as edn]
[promesa.core :as p]
[mount.core :as mount] [mount.core :as mount]
[datoteka.core :as fs] [datoteka.core :as fs]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -63,7 +62,6 @@
([code] ([code]
(System/exit code))) (System/exit code)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Icons Libraries Importer ;; Icons Libraries Importer
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -71,10 +69,8 @@
(defn- icon-library-exists? (defn- icon-library-exists?
[conn id] [conn id]
(s/assert ::us/uuid id) (s/assert ::us/uuid id)
(let [sql "select id from icon_library where id = $1"] (let [row (db/get-by-id conn :icon-library id)]
(-> (db/query-one conn [sql id]) (if row true false)))
(p/then (fn [row] (if row true false))))))
(defn- create-icons-library (defn- create-icons-library
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
@ -87,11 +83,9 @@
(defn- create-icons-library-if-not-exists (defn- create-icons-library-if-not-exists
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
(let [id (uuid/namespaced +icons-uuid-ns+ name)] (let [id (uuid/namespaced +icons-uuid-ns+ name)]
(-> (icon-library-exists? conn id) (when-not (icon-library-exists? conn id)
(p/then (fn [exists?] (create-icons-library conn item))
(when-not exists? id))
(create-icons-library conn item))))
(p/then (constantly id)))))
(defn- create-icon (defn- create-icon
[conn library-id icon-id localpath] [conn library-id icon-id localpath]
@ -113,24 +107,21 @@
(defn- icon-exists? (defn- icon-exists?
[conn id] [conn id]
(s/assert ::us/uuid id) (s/assert ::us/uuid id)
(let [sql "select id from icon where id = $1"] (let [row (db/get-by-id conn :icon id)]
(-> (db/query-one conn [sql id]) (if row true false)))
(p/then (fn [row] (if row true false))))))
(defn- import-icon-if-not-exists (defn- import-icon-if-not-exists
[conn library-id fpath] [conn library-id fpath]
(s/assert ::us/uuid library-id) (s/assert ::us/uuid library-id)
(s/assert fs/path? fpath) (s/assert fs/path? fpath)
(let [icon-id (uuid/namespaced +icons-uuid-ns+ (str library-id (fs/name fpath)))] (let [icon-id (uuid/namespaced +icons-uuid-ns+ (str library-id (fs/name fpath)))]
(-> (icon-exists? conn icon-id) (when-not (icon-exists? conn icon-id)
(p/then (fn [exists?] (create-icon conn library-id icon-id fpath))
(when-not exists? icon-id))
(create-icon conn library-id icon-id fpath))))
(p/then (constantly icon-id)))))
(defn- import-icons (defn- import-icons
[conn library-id {:keys [path regex] :as item}] [conn library-id {:keys [path regex] :as item}]
(p/run! (fn [fpath] (run! (fn [fpath]
(when (re-matches regex (str fpath)) (when (re-matches regex (str fpath))
(import-icon-if-not-exists conn library-id fpath))) (import-icon-if-not-exists conn library-id fpath)))
(->> (fs/list-dir path) (->> (fs/list-dir path)
@ -139,10 +130,9 @@
(defn- process-icons-library (defn- process-icons-library
[conn basedir {:keys [path regex] :as item}] [conn basedir {:keys [path regex] :as item}]
(s/assert ::import-item-media item) (s/assert ::import-item-media item)
(-> (create-icons-library-if-not-exists conn item) (let [library-id (create-icons-library-if-not-exists conn item)]
(p/then (fn [library-id]
(->> (assoc item :path (fs/join basedir path)) (->> (assoc item :path (fs/join basedir path))
(import-icons conn library-id)))))) (import-icons conn library-id))))
;; --- Images Libraries Importer ;; --- Images Libraries Importer
@ -150,9 +140,8 @@
(defn- image-library-exists? (defn- image-library-exists?
[conn id] [conn id]
(s/assert ::us/uuid id) (s/assert ::us/uuid id)
(let [sql "select id from image_library where id = $1"] (let [row (db/get-by-id conn :image-library id)]
(-> (db/query-one conn [sql id]) (if row true false)))
(p/then (fn [row] (if row true false))))))
(defn- create-images-library (defn- create-images-library
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
@ -162,16 +151,12 @@
:team-id uuid/zero :team-id uuid/zero
:name name}))) :name name})))
(defn- create-images-library-if-not-exists (defn- create-images-library-if-not-exists
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
(let [id (uuid/namespaced +images-uuid-ns+ name)] (let [id (uuid/namespaced +images-uuid-ns+ name)]
(-> (image-library-exists? conn id) (when-not (image-library-exists? conn id)
(p/then (fn [exists?] (create-images-library conn item)
(when-not exists? id)))
(create-images-library conn item))))
(p/then (constantly id)))))
(defn- create-image (defn- create-image
[conn library-id image-id localpath] [conn library-id image-id localpath]
@ -186,9 +171,9 @@
".png" "image/png" ".png" "image/png"
".webp" "image/webp")] ".webp" "image/webp")]
(log/info "Creating image" filename image-id) (log/info "Creating image" filename image-id)
(images/create-image conn {:content {:path localpath (images/create-image conn {:content {:tempfile localpath
:name filename :filename filename
:mtype mtype :content-type mtype
:size (.length file)} :size (.length file)}
:id image-id :id image-id
:library-id library-id :library-id library-id
@ -198,24 +183,21 @@
(defn- image-exists? (defn- image-exists?
[conn id] [conn id]
(s/assert ::us/uuid id) (s/assert ::us/uuid id)
(let [sql "select id from image where id = $1"] (let [row (db/get-by-id conn :image id)]
(-> (db/query-one conn [sql id]) (if row true false)))
(p/then (fn [row] (if row true false))))))
(defn- import-image-if-not-exists (defn- import-image-if-not-exists
[conn library-id fpath] [conn library-id fpath]
(s/assert ::us/uuid library-id) (s/assert ::us/uuid library-id)
(s/assert fs/path? fpath) (s/assert fs/path? fpath)
(let [image-id (uuid/namespaced +images-uuid-ns+ (str library-id (fs/name fpath)))] (let [image-id (uuid/namespaced +images-uuid-ns+ (str library-id (fs/name fpath)))]
(-> (image-exists? conn image-id) (when-not (image-exists? conn image-id)
(p/then (fn [exists?] (create-image conn library-id image-id fpath))
(when-not exists? image-id))
(create-image conn library-id image-id fpath))))
(p/then (constantly image-id)))))
(defn- import-images (defn- import-images
[conn library-id {:keys [path regex] :as item}] [conn library-id {:keys [path regex] :as item}]
(p/run! (fn [fpath] (run! (fn [fpath]
(when (re-matches regex (str fpath)) (when (re-matches regex (str fpath))
(import-image-if-not-exists conn library-id fpath))) (import-image-if-not-exists conn library-id fpath)))
(->> (fs/list-dir path) (->> (fs/list-dir path)
@ -224,10 +206,9 @@
(defn- process-images-library (defn- process-images-library
[conn basedir {:keys [path regex] :as item}] [conn basedir {:keys [path regex] :as item}]
(s/assert ::import-item-media item) (s/assert ::import-item-media item)
(-> (create-images-library-if-not-exists conn item) (let [library-id (create-images-library-if-not-exists conn item)]
(p/then (fn [library-id]
(->> (assoc item :path (fs/join basedir path)) (->> (assoc item :path (fs/join basedir path))
(import-images conn library-id)))))) (import-images conn library-id))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -237,9 +218,8 @@
(defn- color-library-exists? (defn- color-library-exists?
[conn id] [conn id]
(s/assert ::us/uuid id) (s/assert ::us/uuid id)
(let [sql "select id from color_library where id = $1"] (let [row (db/get-by-id conn :color-library id)]
(-> (db/query-one conn [sql id]) (if row true false)))
(p/then (fn [row] (if row true false))))))
(defn- create-colors-library (defn- create-colors-library
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
@ -253,43 +233,33 @@
(defn- create-colors-library-if-not-exists (defn- create-colors-library-if-not-exists
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
(let [id (uuid/namespaced +colors-uuid-ns+ name)] (let [id (uuid/namespaced +colors-uuid-ns+ name)]
(-> (color-library-exists? conn id) (when-not (color-library-exists? conn id)
(p/then (fn [exists?] (create-colors-library conn item))
(when-not exists? id))
(create-colors-library conn item))))
(p/then (constantly id)))))
(defn- create-color (defn- create-color
[conn library-id content] [conn library-id content]
(s/assert ::us/uuid library-id) (s/assert ::us/uuid library-id)
(s/assert ::us/color content) (s/assert ::us/color content)
(let [color-id (uuid/namespaced +colors-uuid-ns+ (str library-id content))] (let [color-id (uuid/namespaced +colors-uuid-ns+ (str library-id content))]
(log/info "Creating color" content color-id) (log/info "Creating color" content color-id)
(-> (colors/create-color conn {:id color-id (colors/create-color conn {:id color-id
:library-id library-id :library-id library-id
:name content :name content
:content content}) :content content})
(p/then' (constantly color-id))))) color-id))
(defn- prune-colors
[conn library-id]
(-> (db/query-one conn ["delete from color where library_id=$1" library-id])
(p/then (constantly nil))))
(defn- import-colors (defn- import-colors
[conn library-id {:keys [colors] :as item}] [conn library-id {:keys [colors] :as item}]
(us/verify ::import-item-color item) (us/verify ::import-item-color item)
(p/do! (db/delete! conn :color {:library-id library-id})
(prune-colors conn library-id) (run! #(create-color conn library-id %) colors))
(p/run! #(create-color conn library-id %) colors)))
(defn- process-colors-library (defn- process-colors-library
[conn {:keys [name id colors] :as item}] [conn {:keys [name id colors] :as item}]
(us/verify ::import-item-color item) (us/verify ::import-item-color item)
(-> (create-colors-library-if-not-exists conn item) (let [library-id (create-colors-library-if-not-exists conn item)]
(p/then #(import-colors conn % item)))) (import-colors conn library-id item)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry Point ;; Entry Point
@ -328,22 +298,22 @@
(let [images (:images data) (let [images (:images data)
icons (:icons data) icons (:icons data)
colors (:colors data)] colors (:colors data)]
(p/do! (run! #(process-images-library conn basedir %) images)
(p/run! #(process-images-library conn basedir %) images) (run! #(process-icons-library conn basedir %) icons)
(p/run! #(process-icons-library conn basedir %) icons) (run! #(process-colors-library conn %) colors)))
(p/run! #(process-colors-library conn %) colors)
nil)))
(defn run (defn run
[path] [path]
(p/let [[basedir data] (read-file path)] (let [[basedir data] (read-file path)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(importer conn basedir data)))) (importer conn basedir data))))
(defn -main (defn -main
[& [path]] [& [path]]
(let [path (validate-path path)] (let [path (validate-path path)]
(try
(start-system) (start-system)
(-> (run path) (run path)
(p/finally (fn [_ _] (stop-system)))))) (finally
(stop-system)))))

View file

@ -37,8 +37,9 @@
(defn migrate (defn migrate
[] []
(with-open [ctx (mg/context db/pool)] (with-open [conn (db/open)]
@(mg/migrate ctx +migrations+))) (mg/setup! conn)
(mg/migrate! conn +migrations+)))
(defstate migrations (defstate migrations
:start (migrate)) :start (migrate))

View file

@ -10,13 +10,10 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[lambdaisland.uri :refer [uri]] [lambdaisland.uri :refer [uri]]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[promesa.core :as p]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.core :refer [system]]
[uxbox.util.redis :as redis]
[uxbox.util.data :as data] [uxbox.util.data :as data]
[vertx.util :as vu]) [uxbox.util.redis :as redis])
(:import (:import
java.lang.AutoCloseable)) java.lang.AutoCloseable))
@ -33,20 +30,20 @@
:stop (.close ^AutoCloseable client)) :stop (.close ^AutoCloseable client))
(defstate conn (defstate conn
:start @(redis/connect client) :start (redis/connect client)
:stop (.close ^AutoCloseable conn)) :stop (.close ^AutoCloseable conn))
;; --- API FORWARD ;; --- API FORWARD
(defn subscribe (defn subscribe
[topic] ([topic]
(redis/subscribe client topic)) (redis/subscribe client topic))
([topic xf]
(redis/subscribe client topic xf)))
(defn run! (defn run!
[cmd params] [cmd params]
(let [ctx (vu/get-or-create-context system)] (redis/run! conn cmd params))
(-> (redis/run! conn cmd params)
(vu/handle-on-context ctx))))
(defn run (defn run
[cmd params] [cmd params]

View file

@ -10,15 +10,15 @@
(ns uxbox.services.mutations.colors (ns uxbox.services.mutations.colors
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.tasks :as tasks]
[uxbox.services.queries.teams :as teams]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su] [uxbox.services.queries.teams :as teams]
[uxbox.common.uuid :as uuid])) [uxbox.tasks :as tasks]
[uxbox.util.time :as dt]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -44,15 +44,13 @@
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(create-library conn params))) (create-library conn params)))
(def ^:private sql:create-library
"insert into color_library (id, team_id, name)
values ($1, $2, $3)
returning *;")
(defn create-library (defn create-library
[conn {:keys [id team-id name]}] [conn {:keys [id team-id name]}]
(let [id (or id (uuid/next))] (let [id (or id (uuid/next))]
(db/query-one conn [sql:create-library id team-id name]))) (db/insert! conn :color-library
{:id id
:team-id team-id
:name name})))
;; --- Mutation: Rename Library ;; --- Mutation: Rename Library
@ -66,7 +64,7 @@
(sm/defmutation ::rename-color-library (sm/defmutation ::rename-color-library
[{:keys [id profile-id name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(rename-library conn id name)))) (rename-library conn id name))))
@ -83,42 +81,13 @@
(defn- select-library-for-update (defn- select-library-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-library-for-update id]) (db/get-by-id conn :color-library id {:for-update true}))
(p/then' su/raise-not-found-if-nil)))
(defn- rename-library (defn- rename-library
[conn id name] [conn id name]
(-> (db/query-one conn [sql:rename-library id name]) (db/update! conn :color-library
(p/then' su/constantly-nil))) {:name name}
{:id id}))
;; --- Copy Color
;; (declare create-color)
;; (defn- retrieve-color
;; [conn {:keys [profile-id id]}]
;; (let [sql "select * from color
;; where id = $1
;; and deleted_at is null
;; and (profile_id = $2 or
;; profile_id = '00000000-0000-0000-0000-000000000000'::uuid)"]
;; (-> (db/query-one conn [sql id profile-id])
;; (p/then' su/raise-not-found-if-nil))))
;; (s/def ::copy-color
;; (s/keys :req-un [:us/id ::library-id ::profile-id]))
;; (sm/defmutation ::copy-color
;; [{:keys [profile-id id library-id] :as params}]
;; (db/with-atomic [conn db/pool]
;; (-> (retrieve-color conn {:profile-id profile-id :id id})
;; (p/then (fn [color]
;; (let [color (-> (dissoc color :id)
;; (assoc :library-id library-id))]
;; (create-color conn color)))))))
;; --- Delete Library ;; --- Delete Library
@ -131,7 +100,7 @@
(sm/defmutation ::delete-color-library (sm/defmutation ::delete-color-library
[{:keys [id profile-id] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
;; Schedule object deletion ;; Schedule object deletion
@ -139,18 +108,10 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :color-library}}) :props {:id id :type :color-library}})
(delete-library conn id)))) (db/update! conn :color-library
{:deleted-at (dt/now)}
(def ^:private sql:mark-library-deleted {:id id})
"update color_library nil)))
set deleted_at = clock_timestamp()
where id = $1")
(defn- delete-library
[conn id]
(-> (db/query-one conn [sql:mark-library-deleted id])
(p/then' su/constantly-nil)))
;; --- Mutation: Create Color (Upload) ;; --- Mutation: Create Color (Upload)
@ -164,7 +125,7 @@
(sm/defmutation ::create-color (sm/defmutation ::create-color
[{:keys [profile-id library-id] :as params}] [{:keys [profile-id library-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn library-id)] (let [lib (select-library-for-update conn library-id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(create-color conn params)))) (create-color conn params))))
@ -175,14 +136,15 @@
(defn create-color (defn create-color
[conn {:keys [id name library-id content]}] [conn {:keys [id name library-id content]}]
(let [id (or id (uuid/next))] (let [id (or id (uuid/next))]
(db/query-one conn [sql:create-color id name library-id content]))) (db/insert! conn :color {:id id
:name name
:library-id library-id
:content content})))
;; --- Mutation: Rename Color ;; --- Mutation: Rename Color
(declare select-color-for-update) (declare select-color-for-update)
(declare rename-color)
(s/def ::rename-color (s/def ::rename-color
(s/keys :req-un [::id ::profile-id ::name])) (s/keys :req-un [::id ::profile-id ::name]))
@ -190,33 +152,26 @@
(sm/defmutation ::rename-color (sm/defmutation ::rename-color
[{:keys [id profile-id name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [clr (select-color-for-update conn id)] (let [clr (select-color-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id clr)) (teams/check-edition-permissions! conn profile-id (:team-id clr))
(rename-color conn id name)))) (db/update! conn :color
{:name name}
{:id id}))))
(def ^:private sql:select-color-for-update (def ^:private sql:select-color-for-update
"select c.*, "select c.*,
lib.team_id as team_id lib.team_id as team_id
from color as c from color as c
inner join color_library as lib on (lib.id = c.library_id) inner join color_library as lib on (lib.id = c.library_id)
where c.id = $1 where c.id = ?
for update of c") for update of c")
(def ^:private sql:rename-color
"update color
set name = $2
where id = $1")
(defn- select-color-for-update (defn- select-color-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-color-for-update id]) (let [row (db/exec-one! conn [sql:select-color-for-update id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
(defn- rename-color row))
[conn id name]
(-> (db/query-one conn [sql:rename-color id name])
(p/then' su/constantly-nil)))
;; --- Delete Color ;; --- Delete Color
@ -229,7 +184,7 @@
(sm/defmutation ::delete-color (sm/defmutation ::delete-color
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [clr (select-color-for-update conn id)] (let [clr (select-color-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id clr)) (teams/check-edition-permissions! conn profile-id (:team-id clr))
;; Schedule object deletion ;; Schedule object deletion
@ -237,14 +192,7 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :color}}) :props {:id id :type :color}})
(delete-color conn id)))) (db/update! conn :color
{:deleted-at (dt/now)}
(def ^:private sql:mark-color-deleted {:id id})
"update color nil)))
set deleted_at = clock_timestamp()
where id = $1")
(defn- delete-color
[conn id]
(-> (db/query-one conn [sql:mark-color-deleted id])
(p/then' su/constantly-nil)))

View file

@ -10,25 +10,24 @@
(ns uxbox.services.mutations.files (ns uxbox.services.mutations.files
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[datoteka.core :as fs] [datoteka.core :as fs]
[promesa.core :as p]
[uxbox.common.exceptions :as ex]
[uxbox.common.pages :as cp]
[uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.common.exceptions :as ex] [uxbox.media :as media]
[uxbox.common.spec :as us]
[uxbox.common.pages :as cp]
[uxbox.tasks :as tasks]
[uxbox.services.queries.files :as files]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.mutations.projects :as proj]
[uxbox.services.mutations.images :as imgs] [uxbox.services.mutations.images :as imgs]
[uxbox.services.util :as su] [uxbox.services.mutations.projects :as proj]
[uxbox.services.queries.files :as files]
[uxbox.tasks :as tasks]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.common.uuid :as uuid]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]
[vertx.util :as vu])) [uxbox.util.time :as dt]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -49,41 +48,36 @@
(sm/defmutation ::create-file (sm/defmutation ::create-file
[{:keys [profile-id project-id] :as params}] [{:keys [profile-id project-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [file (create-file conn params) (let [file (create-file conn params)
page (create-page conn (assoc params :file-id (:id file)))] page (create-page conn (assoc params :file-id (:id file)))]
(assoc file :pages [(:id page)])))) (assoc file :pages [(:id page)]))))
(def ^:private sql:create-file
"insert into file (id, project_id, name)
values ($1, $2, $3) returning *")
(def ^:private sql:create-file-profile
"insert into file_profile_rel (profile_id, file_id, is_owner, is_admin, can_edit)
values ($1, $2, true, true, true) returning *")
(def ^:private sql:create-page
"insert into page (id, file_id, name, ordering, data)
values ($1, $2, $3, $4, $5) returning id")
(defn- create-file-profile (defn- create-file-profile
[conn {:keys [profile-id file-id] :as params}] [conn {:keys [profile-id file-id] :as params}]
(db/query-one conn [sql:create-file-profile profile-id file-id])) (db/insert! conn :file-profile-rel
{:profile-id profile-id
:file-id file-id
:is-owner true
:is-admin true
:can-edit true}))
(defn- create-file (defn- create-file
[conn {:keys [id profile-id name project-id] :as params}] [conn {:keys [id profile-id name project-id] :as params}]
(p/let [id (or id (uuid/next)) (let [id (or id (uuid/next))
file (db/query-one conn [sql:create-file id project-id name])] file (db/insert! conn :file {:id id :project-id project-id :name name})]
(->> (assoc params :file-id id) (->> (assoc params :file-id id)
(create-file-profile conn)) (create-file-profile conn))
file)) file))
(defn- create-page (defn- create-page
[conn {:keys [file-id] :as params}] [conn {:keys [file-id] :as params}]
(let [id (uuid/next) (let [id (uuid/next)]
name "Page 1" (db/insert! conn :page
data (blob/encode cp/default-page-data)] {:id id
(db/query-one conn [sql:create-page id file-id name 1 data]))) :file-id file-id
:name "Page 1"
:ordering 1
:data (blob/encode cp/default-page-data)})))
;; --- Mutation: Rename File ;; --- Mutation: Rename File
@ -99,16 +93,11 @@
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(rename-file conn params))) (rename-file conn params)))
(def ^:private sql:rename-file
"update file
set name = $2
where id = $1
and deleted_at is null
returning *")
(defn- rename-file (defn- rename-file
[conn {:keys [id name] :as params}] [conn {:keys [id name] :as params}]
(db/query-one conn [sql:rename-file id name])) (db/update! conn :file
{:name name}
{:id id}))
;; --- Mutation: Delete Project File ;; --- Mutation: Delete Project File
@ -133,13 +122,15 @@
(def ^:private sql:mark-file-deleted (def ^:private sql:mark-file-deleted
"update file "update file
set deleted_at = clock_timestamp() set deleted_at = clock_timestamp()
where id = $1 where id = ?
and deleted_at is null") and deleted_at is null")
(defn mark-file-deleted (defn mark-file-deleted
[conn {:keys [id] :as params}] [conn {:keys [id] :as params}]
(-> (db/query-one conn [sql:mark-file-deleted id]) (db/update! conn :file
(p/then' su/constantly-nil))) {:deleted-at (dt/now)}
{:id id})
nil)
;; --- Mutation: Upload File Image ;; --- Mutation: Upload File Image
@ -169,31 +160,29 @@
(defn- create-file-image (defn- create-file-image
[conn {:keys [content file-id name] :as params}] [conn {:keys [content file-id name] :as params}]
(when-not (imgs/valid-image-types? (:mtype content)) (when-not (imgs/valid-image-types? (:content-type content))
(ex/raise :type :validation (ex/raise :type :validation
:code :image-type-not-allowed :code :image-type-not-allowed
:hint "Seems like you are uploading an invalid image.")) :hint "Seems like you are uploading an invalid image."))
(p/let [image-opts (vu/blocking (images/info (:path content))) (let [image-opts (images/info (:tempfile content))
image-path (imgs/persist-image-on-fs content) image-path (imgs/persist-image-on-fs content)
thumb-opts imgs/thumbnail-options thumb-opts imgs/thumbnail-options
thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path) thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path)]
(-> (db/insert! conn :file-image
sqlv [sql:insert-file-image {:file-id file-id
file-id :name name
name :path (str image-path)
(str image-path) :width (:width image-opts)
(:width image-opts) :height (:height image-opts)
(:height image-opts) :mtype (:content-type content)
(:mtype content) :thumb-path (str thumb-path)
(str thumb-path) :thumb-width (:width thumb-opts)
(:width thumb-opts) :thumb-height (:height thumb-opts)
(:height thumb-opts) :thumb-quality (:quality thumb-opts)
(:quality thumb-opts) :thumb-mtype (images/format->mtype (:format thumb-opts))})
(images/format->mtype (:format thumb-opts))]] (images/resolve-urls :path :uri)
(-> (db/query-one db/pool sqlv) (images/resolve-urls :thumb-path :thumb-uri))))
(p/then' #(images/resolve-urls % :path :uri))
(p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))))
;; --- Mutation: Import from collection ;; --- Mutation: Import from collection
@ -215,28 +204,26 @@
(defn- import-image-to-file (defn- import-image-to-file
[conn {:keys [image-id file-id] :as params}] [conn {:keys [image-id file-id] :as params}]
(p/let [image (-> (db/query-one conn [sql:select-image-by-id image-id]) (let [image (db/get-by-id conn :image image-id)
(p/then' su/raise-not-found-if-nil))
image-path (copy-image (:path image)) image-path (copy-image (:path image))
thumb-path (copy-image (:thumb-path image)) thumb-path (copy-image (:thumb-path image))]
sqlv [sql:insert-file-image
file-id (-> (db/insert! conn :file-image
(:name image) {:file-id file-id
(str image-path) :name (:name image)
(:width image) :path (str image-path)
(:height image) :width (:width image)
(:mtype image) :height (:height image)
(str thumb-path) :mtype (:mtype image)
(:thumb-width image) :thumb-path (str thumb-path)
(:thumb-height image) :thumb-width (:thumb-width image)
(:thumb-quality image) :thumb-height (:thumb-height image)
(:thumb-mtype image)]] :thumb-quality (:thumb-quality image)
(-> (db/query-one db/pool sqlv) :thumb-mtype (:thumb-mtype image)})
(p/then' #(images/resolve-urls % :path :uri)) (images/resolve-urls :path :uri)
(p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) (images/resolve-urls :thumb-path :thumb-uri))))
(defn- copy-image (defn- copy-image
[path] [path]
(vu/blocking
(let [image-path (ust/lookup media/media-storage path)] (let [image-path (ust/lookup media/media-storage path)]
(ust/save! media/media-storage (fs/name image-path) image-path)))) (ust/save! media/media-storage (fs/name image-path) image-path)))

View file

@ -10,17 +10,17 @@
(ns uxbox.services.mutations.icons (ns uxbox.services.mutations.icons
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries.icons :refer [decode-row]] [uxbox.services.queries.icons :refer [decode-row]]
[uxbox.services.queries.teams :as teams] [uxbox.services.queries.teams :as teams]
[uxbox.services.util :as su]
[uxbox.tasks :as tasks] [uxbox.tasks :as tasks]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.common.uuid :as uuid])) [uxbox.util.time :as dt]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -44,7 +44,6 @@
(s/keys :opt-un [::width ::height ::view-box ::mimetype])) (s/keys :opt-un [::width ::height ::view-box ::mimetype]))
;; --- Mutation: Create Library ;; --- Mutation: Create Library
(declare create-library) (declare create-library)
@ -67,8 +66,10 @@
(defn create-library (defn create-library
[conn {:keys [team-id id name] :as params}] [conn {:keys [team-id id name] :as params}]
(let [id (or id (uuid/next))] (let [id (or id (uuid/next))]
(db/query-one conn [sql:create-library id team-id name]))) (db/insert! conn :icon-library
{:id id
:team-id team-id
:name name})))
;; --- Mutation: Rename Library ;; --- Mutation: Rename Library
@ -82,59 +83,19 @@
(sm/defmutation ::rename-icon-library (sm/defmutation ::rename-icon-library
[{:keys [id profile-id name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(rename-library conn id name)))) (rename-library conn id name))))
(def ^:private sql:select-library-for-update
"select l.*
from icon_library as l
where l.id = $1
for update")
(def ^:private sql:rename-library
"update icon_library
set name = $2
where id = $1")
(defn- select-library-for-update (defn- select-library-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-library-for-update id]) (db/get-by-id conn :icon-library id {:for-update true}))
(p/then' su/raise-not-found-if-nil)))
(defn- rename-library (defn- rename-library
[conn id name] [conn id name]
(-> (db/query-one conn [sql:rename-library id name]) (db/update! conn :icon-library
(p/then' su/constantly-nil))) {:name name}
{:id id}))
;; ;; --- Copy Icon
;; (declare create-icon)
;; (defn- retrieve-icon
;; [conn {:keys [profile-id id]}]
;; (let [sql "select * from icon
;; where id = $1
;; and deleted_at is null
;; and (profile_id = $2 or
;; profile_id = '00000000-0000-0000-0000-000000000000'::uuid)"]
;; (-> (db/query-one conn [sql id profile-id])
;; (p/then' su/raise-not-found-if-nil))))
;; (s/def ::copy-icon
;; (s/keys :req-un [:us/id ::library-id ::profile-id]))
;; (sm/defmutation ::copy-icon
;; [{:keys [profile-id id library-id] :as params}]
;; (db/with-atomic [conn db/pool]
;; (-> (retrieve-icon conn {:profile-id profile-id :id id})
;; (p/then (fn [icon]
;; (let [icon (-> (dissoc icon :id)
;; (assoc :library-id library-id))]
;; (create-icon conn icon)))))))
;; --- Mutation: Delete Library ;; --- Mutation: Delete Library
@ -146,7 +107,7 @@
(sm/defmutation ::delete-icon-library (sm/defmutation ::delete-icon-library
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
;; Schedule object deletion ;; Schedule object deletion
@ -154,19 +115,10 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :icon-library}}) :props {:id id :type :icon-library}})
(delete-library conn id)))) (db/update! conn :icon-library
{:deleted-at (dt/now)}
(def ^:private sql:mark-library-deleted {:id id})
"update icon_library nil)))
set deleted_at = clock_timestamp()
where id = $1
returning id")
(defn- delete-library
[conn id]
(-> (db/query-one conn [sql:mark-library-deleted id])
(p/then' su/constantly-nil)))
;; --- Mutation: Create Icon (Upload) ;; --- Mutation: Create Icon (Upload)
@ -180,21 +132,20 @@
(sm/defmutation ::create-icon (sm/defmutation ::create-icon
[{:keys [profile-id library-id] :as params}] [{:keys [profile-id library-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn library-id)] (let [lib (select-library-for-update conn library-id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(create-icon conn params)))) (create-icon conn params))))
(def ^:private sql:create-icon
"insert into icon (id, name, library_id, content, metadata)
values ($1, $2, $3, $4, $5) returning *")
(defn create-icon (defn create-icon
[conn {:keys [id name library-id metadata content]}] [conn {:keys [id name library-id metadata content]}]
(let [id (or id (uuid/next))] (let [id (or id (uuid/next))]
(-> (db/query-one conn [sql:create-icon id name library-id (-> (db/insert! conn :icon
content (blob/encode metadata)]) {:id id
(p/then' decode-row)))) :name name
:library-id library-id
:content content
:metadata (blob/encode metadata)})
(decode-row))))
;; --- Mutation: Rename Icon ;; --- Mutation: Rename Icon
@ -208,33 +159,27 @@
(sm/defmutation ::rename-icon (sm/defmutation ::rename-icon
[{:keys [id profile-id name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [clr (select-icon-for-update conn id)] (let [icon (select-icon-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id clr)) (teams/check-edition-permissions! conn profile-id (:team-id icon))
(rename-icon conn id name)))) (db/update! conn :icon
{:name name}
{:id id}))))
(def ^:private sql:select-icon-for-update (def ^:private
sql:select-icon-for-update
"select i.*, "select i.*,
lib.team_id as team_id lib.team_id as team_id
from icon as i from icon as i
inner join icon_library as lib on (lib.id = i.library_id) inner join icon_library as lib on (lib.id = i.library_id)
where i.id = $1 where i.id = ?
for update") for update")
(def ^:private sql:rename-icon
"update icon
set name = $2
where id = $1")
(defn- select-icon-for-update (defn- select-icon-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-icon-for-update id]) (let [row (db/exec-one! conn [sql:select-icon-for-update id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
(defn- rename-icon row))
[conn id name]
(-> (db/query-one conn [sql:rename-icon id name])
(p/then' su/constantly-nil)))
;; --- Mutation: Delete Icon ;; --- Mutation: Delete Icon
@ -247,7 +192,7 @@
(sm/defmutation ::delete-icon (sm/defmutation ::delete-icon
[{:keys [id profile-id] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [icn (select-icon-for-update conn id)] (let [icn (select-icon-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id icn)) (teams/check-edition-permissions! conn profile-id (:team-id icn))
;; Schedule object deletion ;; Schedule object deletion
@ -255,14 +200,7 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :icon}}) :props {:id id :type :icon}})
(delete-icon conn id)))) (db/update! conn :icon
{:deleted-at (dt/now)}
(def ^:private sql:mark-icon-deleted {:id id})
"update icon nil)))
set deleted_at = clock_timestamp()
where id = $1")
(defn- delete-icon
[conn id]
(-> (db/query-one conn [sql:mark-icon-deleted id])
(p/then' su/constantly-nil)))

View file

@ -21,10 +21,9 @@
[uxbox.tasks :as tasks] [uxbox.tasks :as tasks]
[uxbox.services.queries.teams :as teams] [uxbox.services.queries.teams :as teams]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]
[vertx.util :as vu])) [uxbox.util.time :as dt]))
(def thumbnail-options (def thumbnail-options
{:width 800 {:width 800
@ -53,21 +52,18 @@
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(create-library conn params))) (create-library conn params)))
(def ^:private sql:create-library
"insert into image_library (id, team_id, name)
values ($1, $2, $3)
returning *;")
(defn create-library (defn create-library
[conn {:keys [id team-id name]}] [conn {:keys [id team-id name]}]
(let [id (or id (uuid/next))] (let [id (or id (uuid/next))]
(db/query-one conn [sql:create-library id team-id name]))) (db/insert! conn :image-library
{:id id
:team-id team-id
:name name})))
;; --- Rename Library ;; --- Rename Library
(declare select-library-for-update) (declare select-library-for-update)
(declare rename-library)
(s/def ::rename-image-library (s/def ::rename-image-library
(s/keys :req-un [::id ::profile-id ::name])) (s/keys :req-un [::id ::profile-id ::name]))
@ -75,31 +71,15 @@
(sm/defmutation ::rename-image-library (sm/defmutation ::rename-image-library
[{:keys [profile-id id name] :as params}] [{:keys [profile-id id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(rename-library conn id name)))) (db/update! conn :image-library
{:name name}
(def ^:private sql:select-library-for-update {:id id}))))
"select l.*
from image_library as l
where l.id = $1
for update")
(def ^:private sql:rename-library
"update image_library
set name = $2
where id = $1")
(defn- select-library-for-update (defn- select-library-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-library-for-update id]) (db/get-by-id conn :image-library id {:for-update true}))
(p/then' su/raise-not-found-if-nil)))
(defn- rename-library
[conn id name]
(-> (db/query-one conn [sql:rename-library id name])
(p/then' su/constantly-nil)))
;; --- Delete Library ;; --- Delete Library
@ -112,7 +92,7 @@
(sm/defmutation ::delete-image-library (sm/defmutation ::delete-image-library
[{:keys [id profile-id] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn id)] (let [lib (select-library-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
;; Schedule object deletion ;; Schedule object deletion
@ -120,17 +100,10 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :image-library}}) :props {:id id :type :image-library}})
(delete-library conn id)))) (db/update! conn :image-library
{:deleted-at (dt/now)}
(def ^:private sql:mark-library-deleted {:id id})
"update image_library nil)))
set deleted_at = clock_timestamp()
where id = $1")
(defn- delete-library
[conn id]
(-> (db/query-one conn [sql:mark-library-deleted id])
(p/then' su/constantly-nil)))
@ -143,16 +116,16 @@
(def valid-image-types? (def valid-image-types?
#{"image/jpeg", "image/png", "image/webp"}) #{"image/jpeg", "image/png", "image/webp"})
(s/def :uxbox$upload/name ::us/string) (s/def :uxbox$upload/filename ::us/string)
(s/def :uxbox$upload/size ::us/integer) (s/def :uxbox$upload/size ::us/integer)
(s/def :uxbox$upload/mtype valid-image-types?) (s/def :uxbox$upload/content-type valid-image-types?)
(s/def :uxbox$upload/path ::us/string) (s/def :uxbox$upload/tempfile any?)
(s/def ::upload (s/def ::upload
(s/keys :req-un [:uxbox$upload/name (s/keys :req-un [:uxbox$upload/filename
:uxbox$upload/size :uxbox$upload/size
:uxbox$upload/path :uxbox$upload/tempfile
:uxbox$upload/mtype])) :uxbox$upload/content-type]))
(s/def ::content ::upload) (s/def ::content ::upload)
@ -163,69 +136,54 @@
(sm/defmutation ::upload-image (sm/defmutation ::upload-image
[{:keys [library-id profile-id] :as params}] [{:keys [library-id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (select-library-for-update conn library-id)] (let [lib (select-library-for-update conn library-id)]
(teams/check-edition-permissions! conn profile-id (:team-id lib)) (teams/check-edition-permissions! conn profile-id (:team-id lib))
(create-image conn params)))) (create-image conn params))))
(def ^:private sql:insert-image
"insert into image
(id, library_id, name, path, width, height, mtype,
thumb_path, thumb_width, thumb_height, thumb_quality, thumb_mtype)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
returning *")
(defn create-image (defn create-image
[conn {:keys [id content library-id name]}] [conn {:keys [id content library-id name]}]
(when-not (valid-image-types? (:mtype content)) (when-not (valid-image-types? (:content-type content))
(ex/raise :type :validation (ex/raise :type :validation
:code :image-type-not-allowed :code :image-type-not-allowed
:hint "Seems like you are uploading an invalid image.")) :hint "Seems like you are uploading an invalid image."))
(p/let [image-opts (vu/blocking (images/info (:path content))) (let [image-opts (images/info (:tempfile content))
image-path (persist-image-on-fs content) image-path (persist-image-on-fs content)
thumb-opts thumbnail-options thumb-opts thumbnail-options
thumb-path (persist-image-thumbnail-on-fs thumb-opts image-path) thumb-path (persist-image-thumbnail-on-fs thumb-opts image-path)]
id (or id (uuid/next)) (-> (db/insert! conn :image
{:id (or id (uuid/next))
sqlv [sql:insert-image :library-id library-id
id :name name
library-id :path (str image-path)
name :width (:width image-opts)
(str image-path) :height (:height image-opts)
(:width image-opts) :mtype (:content-type content)
(:height image-opts) :thumb-path (str thumb-path)
(:mtype content) :thumb-width (:width thumb-opts)
(str thumb-path) :thumb-height (:height thumb-opts)
(:width thumb-opts) :thumb-quality (:quality thumb-opts)
(:height thumb-opts) :thumb-mtype (images/format->mtype (:format thumb-opts))})
(:quality thumb-opts) (images/resolve-urls :path :uri)
(images/format->mtype (:format thumb-opts))]] (images/resolve-urls :thumb-path :thumb-uri))))
(-> (db/query-one conn sqlv)
(p/then' #(images/resolve-urls % :path :uri))
(p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))))
(defn persist-image-on-fs (defn persist-image-on-fs
[{:keys [name path]}] [{:keys [filename tempfile]}]
(vu/blocking (let [filename (fs/name filename)]
(let [filename (fs/name name)] (ust/save! media/media-storage filename tempfile)))
(ust/save! media/media-storage filename path))))
(defn persist-image-thumbnail-on-fs (defn persist-image-thumbnail-on-fs
[thumb-opts input-path] [thumb-opts input-path]
(vu/blocking
(let [input-path (ust/lookup media/media-storage input-path) (let [input-path (ust/lookup media/media-storage input-path)
thumb-data (images/generate-thumbnail input-path thumb-opts) thumb-data (images/generate-thumbnail input-path thumb-opts)
[filename _] (fs/split-ext (fs/name input-path)) [filename _] (fs/split-ext (fs/name input-path))
thumb-name (->> (images/format->extension (:format thumb-opts)) thumb-name (->> (images/format->extension (:format thumb-opts))
(str "thumbnail-" filename))] (str "thumbnail-" filename))]
(ust/save! media/media-storage thumb-name thumb-data)))) (ust/save! media/media-storage thumb-name thumb-data)))
;; --- Mutation: Rename Image ;; --- Mutation: Rename Image
(declare select-image-for-update) (declare select-image-for-update)
(declare rename-image)
(s/def ::rename-image (s/def ::rename-image
(s/keys :req-un [::id ::profile-id ::name])) (s/keys :req-un [::id ::profile-id ::name]))
@ -233,70 +191,38 @@
(sm/defmutation ::rename-image (sm/defmutation ::rename-image
[{:keys [id profile-id name] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [img (select-image-for-update conn id)] (let [img (select-image-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id img)) (teams/check-edition-permissions! conn profile-id (:team-id img))
(rename-image conn id name)))) (db/update! conn :image
{:name name}
{:id id}))))
(def ^:private sql:select-image-for-update (def ^:private sql:select-image-for-update
"select img.*, "select img.*,
lib.team_id as team_id lib.team_id as team_id
from image as img from image as img
inner join image_library as lib on (lib.id = img.library_id) inner join image_library as lib on (lib.id = img.library_id)
where img.id = $1 where img.id = ?
for update of img") for update of img")
(def ^:private sql:rename-image
"update image
set name = $2
where id = $1")
(defn- select-image-for-update (defn- select-image-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-image-for-update id]) (let [row (db/exec-one! conn [sql:select-image-for-update id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))
(defn- rename-image
[conn id name]
(-> (db/query-one conn [sql:rename-image id name])
(p/then' su/constantly-nil)))
;; --- Copy Image
;; (declare retrieve-image)
;; (s/def ::copy-image
;; (s/keys :req-un [::id ::library-id ::profile-id]))
;; (sm/defmutation ::copy-image
;; [{:keys [profile-id id library-id] :as params}]
;; (letfn [(copy-image [conn {:keys [path] :as image}]
;; (-> (ds/lookup media/images-storage (:path image))
;; (p/then (fn [path] (ds/save media/images-storage (fs/name path) path)))
;; (p/then (fn [path]
;; (-> image
;; (assoc :path (str path) :library-id library-id)
;; (dissoc :id))))
;; (p/then (partial store-image-in-db conn))))]
;; (db/with-atomic [conn db/pool]
;; (-> (retrieve-image conn {:id id :profile-id profile-id})
;; (p/then su/raise-not-found-if-nil)
;; (p/then (partial copy-image conn))))))
;; --- Delete Image ;; --- Delete Image
(declare delete-image)
(s/def ::delete-image (s/def ::delete-image
(s/keys :req-un [::id ::profile-id])) (s/keys :req-un [::id ::profile-id]))
(sm/defmutation ::delete-image (sm/defmutation ::delete-image
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [img (select-image-for-update conn id)] (let [img (select-image-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id img)) (teams/check-edition-permissions! conn profile-id (:team-id img))
;; Schedule object deletion ;; Schedule object deletion
@ -304,14 +230,7 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :image}}) :props {:id id :type :image}})
(delete-image conn id)))) (db/update! conn :image
{:deleted-at (dt/now)}
(def ^:private sql:mark-image-deleted {:id id})
"update image nil)))
set deleted_at = clock_timestamp()
where id = $1")
(defn- delete-image
[conn id]
(-> (db/query-one conn [sql:mark-image-deleted id])
(p/then' su/constantly-nil)))

View file

@ -10,22 +10,19 @@
(ns uxbox.services.mutations.pages (ns uxbox.services.mutations.pages
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.common.data :as d] [uxbox.common.data :as d]
[uxbox.common.pages :as cp]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.pages :as cp]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.services.queries.files :as files]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries.files :as files]
[uxbox.services.queries.pages :refer [decode-row]] [uxbox.services.queries.pages :refer [decode-row]]
[uxbox.services.util :as su]
[uxbox.tasks :as tasks] [uxbox.tasks :as tasks]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.util.sql :as sql] [uxbox.util.time :as dt]))
[uxbox.common.uuid :as uuid]
[vertx.eventbus :as ve]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -51,20 +48,17 @@
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(create-page conn params))) (create-page conn params)))
(def ^:private sql:create-page
"insert into page (id, file_id, name, ordering, data)
values ($1, $2, $3, $4, $5)
returning *")
(defn- create-page (defn- create-page
[conn {:keys [id file-id name ordering data] :as params}] [conn {:keys [id file-id name ordering data] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
data (blob/encode data)] data (blob/encode data)]
(-> (db/query-one conn [sql:create-page (-> (db/insert! conn :page
id file-id name {:id id
ordering data]) :file-id file-id
(p/then' decode-row)))) :name name
:ordering ordering
:data data})
(decode-row))))
;; --- Mutation: Rename Page ;; --- Mutation: Rename Page
@ -78,33 +72,19 @@
(sm/defmutation ::rename-page (sm/defmutation ::rename-page
[{:keys [id name profile-id]}] [{:keys [id name profile-id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [page (select-page-for-update conn id)] (let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page)) (files/check-edition-permissions! conn profile-id (:file-id page))
(rename-page conn (assoc page :name name))))) (rename-page conn (assoc page :name name)))))
(def ^:private sql:select-page-for-update
"select p.id, p.revn, p.file_id, p.data
from page as p
where p.id = $1
and deleted_at is null
for update;")
(defn- select-page-for-update (defn- select-page-for-update
[conn id] [conn id]
(-> (db/query-one conn [sql:select-page-for-update id]) (db/get-by-id conn :page id {:for-update true}))
(p/then' su/raise-not-found-if-nil)))
(def ^:private sql:rename-page
"update page
set name = $2
where id = $1
and deleted_at is null")
(defn- rename-page (defn- rename-page
[conn {:keys [id name] :as params}] [conn {:keys [id name] :as params}]
(-> (db/query-one conn [sql:rename-page id name]) (db/update! conn :page
(p/then su/constantly-nil))) {:name name}
{:id id}))
;; --- Mutation: Sort Pages ;; --- Mutation: Sort Pages
@ -118,20 +98,16 @@
(sm/defmutation ::reorder-pages (sm/defmutation ::reorder-pages
[{:keys [profile-id file-id page-ids]}] [{:keys [profile-id file-id page-ids]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/run! #(update-page-ordering conn file-id %) (run! #(update-page-ordering conn file-id %)
(d/enumerate page-ids)) (d/enumerate page-ids))
nil)) nil))
(def ^:private sql:update-page-ordering
"update page
set ordering = $1
where id = $2 and file_id = $3")
(defn- update-page-ordering (defn- update-page-ordering
[conn file-id [ordering page-id]] [conn file-id [ordering page-id]]
(-> (db/query-one conn [sql:update-page-ordering ordering page-id file-id]) (db/update! conn :page
(p/then su/constantly-nil))) {:ordering ordering}
{:file-id file-id
:id page-id}))
;; --- Mutation: Generate Share Token ;; --- Mutation: Generate Share Token
@ -146,16 +122,9 @@
(let [token (-> (sodi.prng/random-bytes 16) (let [token (-> (sodi.prng/random-bytes 16)
(sodi.util/bytes->b64s))] (sodi.util/bytes->b64s))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(assign-page-share-token conn id token)))) (db/update! conn :page
{:share-token token}
(def ^:private sql:update-page-share-token {:id id}))))
"update page set share_token = $2 where id = $1")
(defn- assign-page-share-token
[conn id token]
(-> (db/query-one conn [sql:update-page-share-token id token])
(p/then (fn [_] {:id id :share-token token}))))
;; --- Mutation: Clear Share Token ;; --- Mutation: Clear Share Token
@ -166,7 +135,9 @@
(sm/defmutation ::clear-page-share-token (sm/defmutation ::clear-page-share-token
[{:keys [id] :as params}] [{:keys [id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(assign-page-share-token conn id nil))) (db/update! conn :page
{:share-token nil}
{:id id})))
@ -183,13 +154,12 @@
(declare update-page) (declare update-page)
(declare retrieve-lagged-changes) (declare retrieve-lagged-changes)
(declare update-page-data) (declare insert-page-change!)
(declare insert-page-change)
(sm/defmutation ::update-page (sm/defmutation ::update-page
[{:keys [id profile-id] :as params}] [{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [{:keys [file-id] :as page} (select-page-for-update conn id)] (let [{:keys [file-id] :as page} (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(update-page conn page params)))) (update-page conn page params))))
@ -211,61 +181,52 @@
page (assoc page page (assoc page
:data data :data data
:revn (inc (:revn page)) :revn (inc (:revn page))
:changes (blob/encode changes))] :changes (blob/encode changes))
(-> (update-page-data conn page) chng (insert-page-change! conn page)]
(p/then (fn [_] (insert-page-change conn page)))
(p/then (fn [s]
(let [topic (str "internal.uxbox.file." (:file-id page))]
(p/do! (ve/publish! uxbox.core/system topic
{:type :page-change
:profile-id (:profile-id params)
:page-id (:page-id s)
:revn (:revn s)
:changes changes})
(retrieve-lagged-changes conn s params))))))))
(def ^:private sql:update-page-data (db/update! conn :page
"update page {:revn (:revn page)
set revn = $1, :data data}
data = $2 {:id (:id page)})
where id = $3")
(defn- update-page-data (retrieve-lagged-changes conn chng params)))
[conn {:keys [id name revn data]}]
(-> (db/query-one conn [sql:update-page-data revn data id])
(p/then' su/constantly-nil)))
(def ^:private sql:insert-page-change ;; (p/do! (ve/publish! uxbox.core/system topic
"insert into page_change (id, page_id, revn, data, changes) ;; {:type :page-change
values ($1, $2, $3, $4, $5) ;; :profile-id (:profile-id params)
returning id, page_id, revn, changes") ;; :page-id (:page-id s)
;; :revn (:revn s)
;; :changes changes})
(defn- insert-page-change (defn- insert-page-change!
[conn {:keys [revn data changes] :as page}] [conn {:keys [revn data changes] :as page}]
(let [id (uuid/next) (let [id (uuid/next)
page-id (:id page)] page-id (:id page)]
(db/query-one conn [sql:insert-page-change id (db/insert! conn :page-change
page-id revn data changes]))) {:id id
:page-id page-id
:revn revn
:data data
:changes changes})))
(def ^:private sql:lagged-changes (def ^:private
sql:lagged-changes
"select s.id, s.changes "select s.id, s.changes
from page_change as s from page_change as s
where s.page_id = $1 where s.page_id = ?
and s.revn > $2 and s.revn > ?
order by s.created_at asc") order by s.created_at asc")
(defn- retrieve-lagged-changes (defn- retrieve-lagged-changes
[conn snapshot params] [conn snapshot params]
(-> (db/query conn [sql:lagged-changes (:id params) (:revn params)]) (let [rows (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])]
(p/then (fn [rows]
{:page-id (:id params) {:page-id (:id params)
:revn (:revn snapshot) :revn (:revn snapshot)
:changes (into [] (comp (map decode-row) :changes (into [] (comp (map decode-row)
(map :changes) (map :changes)
(mapcat identity)) (mapcat identity))
rows)})))) rows)}))
;; --- Mutation: Delete Page ;; --- Mutation: Delete Page
@ -277,7 +238,7 @@
(sm/defmutation ::delete-page (sm/defmutation ::delete-page
[{:keys [id profile-id]}] [{:keys [id profile-id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [page (select-page-for-update conn id)] (let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page)) (files/check-edition-permissions! conn profile-id (:file-id page))
;; Schedule object deletion ;; Schedule object deletion
@ -285,15 +246,7 @@
:delay cfg/default-deletion-delay :delay cfg/default-deletion-delay
:props {:id id :type :page}}) :props {:id id :type :page}})
(mark-page-deleted conn id)))) (db/update! conn :page
{:deleted-at (dt/now)}
(def ^:private sql:mark-page-deleted {:id id})
"update page nil)))
set deleted_at = clock_timestamp()
where id = $1
and deleted_at is null")
(defn- mark-page-deleted
[conn id]
(-> (db/query-one conn [sql:mark-page-deleted id])
(p/then su/constantly-nil)))

View file

@ -19,23 +19,21 @@
[sodi.util] [sodi.util]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.emails :as emails] [uxbox.emails :as emails]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.tasks :as tasks]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.mutations.images :as imgs] [uxbox.services.mutations.images :as imgs]
[uxbox.services.mutations.teams :as mt.teams]
[uxbox.services.mutations.projects :as mt.projects] [uxbox.services.mutations.projects :as mt.projects]
[uxbox.services.mutations.teams :as mt.teams]
[uxbox.services.queries.profile :as profile] [uxbox.services.queries.profile :as profile]
[uxbox.services.util :as su] [uxbox.tasks :as tasks]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]
[uxbox.common.uuid :as uuid] [uxbox.util.time :as dt]))
[uxbox.util.time :as tm]
[vertx.util :as vu]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -75,22 +73,15 @@
:code ::wrong-credentials)) :code ::wrong-credentials))
profile)] profile)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [prof (-> (retrieve-profile-by-email conn email) (let [prof (-> (retrieve-profile-by-email conn email)
(p/then' check-profile) (check-profile)
(p/then' profile/strip-private-attrs)) (profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof))] addt (profile/retrieve-additional-data conn (:id prof))]
(merge prof addt))))) (merge prof addt)))))
(def sql:profile-by-email
"select u.*
from profile as u
where u.email=$1
and u.deleted_at is null")
(defn- retrieve-profile-by-email (defn- retrieve-profile-by-email
[conn email] [conn email]
(-> (db/query-one conn [sql:profile-by-email email]) (db/get-by-params conn :profile {:email email} {:for-update true}))
(p/then #(images/resolve-media-uris % [:photo :photo-uri]))))
;; --- Mutation: Update Profile (own) ;; --- Mutation: Update Profile (own)
@ -106,10 +97,11 @@
(defn- update-profile (defn- update-profile
[conn {:keys [id fullname lang theme] :as params}] [conn {:keys [id fullname lang theme] :as params}]
(let [sqlv [sql:update-profile id fullname lang theme]] (db/update! conn :profile
(-> (db/query-one conn sqlv) {:fullname fullname
(p/then' su/raise-not-found-if-nil) :lang lang
(p/then' profile/strip-private-attrs)))) :theme theme}
{:id id}))
(s/def ::update-profile (s/def ::update-profile
(s/keys :req-un [::id ::fullname ::lang ::theme])) (s/keys :req-un [::id ::fullname ::lang ::theme]))
@ -117,39 +109,31 @@
(sm/defmutation ::update-profile (sm/defmutation ::update-profile
[params] [params]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(update-profile conn params))) (update-profile conn params)
nil))
;; --- Mutation: Update Password ;; --- Mutation: Update Password
(defn- validate-password! (defn- validate-password!
[conn {:keys [profile-id old-password] :as params}] [conn {:keys [profile-id old-password] :as params}]
(p/let [profile (profile/retrieve-profile conn profile-id) (let [profile (profile/retrieve-profile conn profile-id)
result (sodi.pwhash/verify old-password (:password profile))] result (sodi.pwhash/verify old-password (:password profile))]
(when-not (:valid result) (when-not (:valid result)
(ex/raise :type :validation (ex/raise :type :validation
:code ::old-password-not-match)))) :code ::old-password-not-match))))
(defn update-password
[conn {:keys [profile-id password]}]
(let [sql "update profile
set password = $2
where id = $1
and deleted_at is null
returning id"
password (sodi.pwhash/derive password)]
(-> (db/query-one conn [sql profile-id password])
(p/then' su/raise-not-found-if-nil)
(p/then' su/constantly-nil))))
(s/def ::update-profile-password (s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password])) (s/keys :req-un [::profile-id ::password ::old-password]))
(sm/defmutation ::update-profile-password (sm/defmutation ::update-profile-password
[params] [{:keys [password profile-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(validate-password! conn params) (validate-password! conn params)
(update-password conn params))) (db/update! conn :profile
{:password (sodi.pwhash/derive password)}
{:id profile-id})
nil))
@ -165,7 +149,7 @@
(sm/defmutation ::update-profile-photo (sm/defmutation ::update-profile-photo
[{:keys [profile-id file] :as params}] [{:keys [profile-id file] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [profile (profile/retrieve-profile conn profile-id) (let [profile (profile/retrieve-profile conn profile-id)
photo (upload-photo conn params)] photo (upload-photo conn params)]
;; Schedule deletion of old photo ;; Schedule deletion of old photo
@ -178,11 +162,10 @@
(defn- upload-photo (defn- upload-photo
[conn {:keys [file profile-id]}] [conn {:keys [file profile-id]}]
(when-not (imgs/valid-image-types? (:mtype file)) (when-not (imgs/valid-image-types? (:content-type file))
(ex/raise :type :validation (ex/raise :type :validation
:code :image-type-not-allowed :code :image-type-not-allowed
:hint "Seems like you are uploading an invalid image.")) :hint "Seems like you are uploading an invalid image."))
(vu/blocking
(let [thumb-opts {:width 256 (let [thumb-opts {:width 256
:height 256 :height 256
:quality 75 :quality 75
@ -190,8 +173,9 @@
prefix (-> (sodi.prng/random-bytes 8) prefix (-> (sodi.prng/random-bytes 8)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))
name (str prefix ".webp") name (str prefix ".webp")
photo (images/generate-thumbnail2 (fs/path (:path file)) thumb-opts)] path (fs/path (:tempfile file))
(ust/save! media/media-storage name photo)))) photo (images/generate-thumbnail2 path thumb-opts)]
(ust/save! media/media-storage name photo)))
(defn- update-profile-photo (defn- update-profile-photo
[conn profile-id path] [conn profile-id path]
@ -199,9 +183,10 @@
where id=$2 where id=$2
and deleted_at is null and deleted_at is null
returning id"] returning id"]
(-> (db/query-one conn [sql (str path) profile-id]) (db/update! conn :profile
(p/then' su/raise-not-found-if-nil)))) {:photo (str path)}
{:id profile-id})
nil))
;; --- Mutation: Register Profile ;; --- Mutation: Register Profile
@ -213,7 +198,8 @@
(s/keys :req-un [::email ::password ::fullname])) (s/keys :req-un [::email ::password ::fullname]))
(defn email-domain-in-whitelist? (defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given whitelist is an empty string." "Returns true if email's domain is in the given whitelist or if given
whitelist is an empty string."
[whitelist email] [whitelist email]
(if (str/blank? whitelist) (if (str/blank? whitelist)
true true
@ -226,20 +212,18 @@
(when-not (:registration-enabled cfg/config) (when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :registration-disabled)) :code :registration-disabled))
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config) (:email params)) (when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
(:email params))
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-domain-is-not-allowed)) :code ::email-domain-is-not-allowed))
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-profile-existence! conn params) (check-profile-existence! conn params)
(-> (register-profile conn params) (let [profile (register-profile conn params)]
(p/then (fn [profile]
;; TODO: send a correct link for email verification ;; TODO: send a correct link for email verification
(let [data {:to (:email params) (let [data {:to (:email params)
:name (:fullname params)}] :name (:fullname params)}]
(p/do!
(emails/send! conn emails/register data) (emails/send! conn emails/register data)
profile))))))) profile))))
(def ^:private sql:insert-profile (def ^:private sql:insert-profile
"insert into profile (id, fullname, email, password, photo, is_demo) "insert into profile (id, fullname, email, password, photo, is_demo)
@ -256,12 +240,11 @@
(defn- check-profile-existence! (defn- check-profile-existence!
[conn {:keys [email] :as params}] [conn {:keys [email] :as params}]
(-> (db/query-one conn [sql:profile-existence email]) (let [result (db/exec-one! conn [sql:profile-existence email])]
(p/then' (fn [result]
(when (:val result) (when (:val result)
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-already-exists)) :code ::email-already-exists))
params)))) params))
(defn- create-profile (defn- create-profile
"Create the profile entry on the database with limited input "Create the profile entry on the database with limited input
@ -270,16 +253,24 @@
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
demo? (if (boolean? demo?) demo? false) demo? (if (boolean? demo?) demo? false)
password (sodi.pwhash/derive password)] password (sodi.pwhash/derive password)]
(db/query-one conn [sql:insert-profile id fullname email password demo?]))) (db/insert! conn :profile
{:id id
:fullname fullname
:email email
:photo ""
:password password
:is-demo demo?})))
(defn- create-profile-email (defn- create-profile-email
[conn {:keys [id email] :as profile}] [conn {:keys [id email] :as profile}]
(-> (db/query-one conn [sql:insert-email id email]) (db/insert! conn :profile-email
(p/then' su/constantly-nil))) {:profile-id id
:email email
:is-main true}))
(defn register-profile (defn register-profile
[conn params] [conn params]
(p/let [prof (create-profile conn params) (let [prof (create-profile conn params)
_ (create-profile-email conn prof) _ (create-profile-email conn prof)
team (mt.teams/create-team conn {:profile-id (:id prof) team (mt.teams/create-team conn {:profile-id (:id prof)
@ -293,10 +284,11 @@
:name "Drafts" :name "Drafts"
:default? true}) :default? true})
_ (mt.projects/create-project-profile conn {:project-id (:id proj) _ (mt.projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id prof)})] :profile-id (:id prof)})
]
(merge (profile/strip-private-attrs prof) (merge (profile/strip-private-attrs prof)
{:default-team team {:default-team (:id team)
:default-project proj}))) :default-project (:id proj)})))
;; --- Mutation: Request Profile Recovery ;; --- Mutation: Request Profile Recovery
@ -312,20 +304,21 @@
(let [token (-> (sodi.prng/random-bytes 32) (let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))
sql sql:insert-recovery-token] sql sql:insert-recovery-token]
(-> (db/query-one conn [sql id token]) (db/insert! conn :password-recovery-token
(p/then (constantly (assoc profile :token token)))))) {:profile-id id
:token token})
(assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
(emails/send! conn (emails/send! conn emails/password-recovery
emails/password-recovery
{:to (:email profile) {:to (:email profile)
:token (:token profile) :token (:token profile)
:name (:fullname profile)}))] :name (:fullname profile)})
nil)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (retrieve-profile-by-email conn email) (let [profile (->> (retrieve-profile-by-email conn email)
(p/then' su/raise-not-found-if-nil) (create-recovery-token conn))]
(p/then #(create-recovery-token conn %)) (send-email-notification conn profile)))))
(p/then #(send-email-notification conn %))
(p/then (constantly nil))))))
;; --- Mutation: Recover Profile ;; --- Mutation: Recover Profile
@ -343,18 +336,17 @@
where token=$1 returning *" where token=$1 returning *"
sql "select * from password_recovery_token sql "select * from password_recovery_token
where token=$1"] where token=$1"]
(-> (db/query-one conn [sql token]) (-> {:token token}
(p/then' :profile-id) (db/get-by-params conn :password-recovery-token)
(p/then' su/raise-not-found-if-nil)))) (:profile-id))))
(update-password [conn profile-id] (update-password [conn profile-id]
(let [sql "update profile set password=$2 where id=$1" (let [sql "update profile set password=$2 where id=$1"
pwd (sodi.pwhash/derive password)] pwd (sodi.pwhash/derive password)]
(-> (db/query-one conn [sql profile-id pwd]) (db/update! conn :profile {:password pwd} {:id profile-id})
(p/then' (constantly nil)))))] nil))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (validate-token conn token) (-> (validate-token conn token)
(p/then (fn [profile-id] (update-password conn profile-id))))))) (update-password conn)))))
;; --- Mutation: Delete Profile ;; --- Mutation: Delete Profile
@ -372,16 +364,19 @@
;; Schedule a complete deletion of profile ;; Schedule a complete deletion of profile
(tasks/schedule! conn {:name "delete-profile" (tasks/schedule! conn {:name "delete-profile"
:delay (tm/duration {:hours 48}) :delay (dt/duration {:hours 48})
:props {:profile-id profile-id}}) :props {:profile-id profile-id}})
(mark-profile-as-deleted! conn profile-id))) (db/update! conn :profile
{:deleted-at (dt/now)}
{:id profile-id})
nil))
(def ^:private sql:teams-ownership-check (def ^:private sql:teams-ownership-check
"with teams as ( "with teams as (
select tpr.team_id as id select tpr.team_id as id
from team_profile_rel as tpr from team_profile_rel as tpr
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.is_owner is true and tpr.is_owner is true
) )
select tpr.team_id, select tpr.team_id,
@ -393,18 +388,9 @@
(defn- check-teams-ownership! (defn- check-teams-ownership!
[conn profile-id] [conn profile-id]
(-> (db/query conn [sql:teams-ownership-check profile-id]) (let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(p/then' (fn [rows]
(when-not (empty? rows) (when-not (empty? rows)
(ex/raise :type :validation (ex/raise :type :validation
:code :owner-teams-with-people :code :owner-teams-with-people
:hint "The user need to transfer ownership of owned teams." :hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)})))))) :context {:teams (mapv :team-id rows)}))))
(def ^:private sql:mark-profile-deleted
"update profile set deleted_at=now() where id=$1")
(defn- mark-profile-as-deleted!
[conn profile-id]
(-> (db/query-one conn [sql:mark-profile-deleted profile-id])
(p/then' su/constantly-nil)))

View file

@ -10,16 +10,14 @@
(ns uxbox.services.mutations.projects (ns uxbox.services.mutations.projects
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.config :as cfg]
[uxbox.db :as db]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.tasks :as tasks] [uxbox.common.uuid :as uuid]
[uxbox.config :as cfg]
[uxbox.db :as db]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su] [uxbox.tasks :as tasks]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]))
[uxbox.common.uuid :as uuid]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -35,28 +33,28 @@
tpr.can_edit tpr.can_edit
from team_profile_rel as tpr from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id) inner join project as p on (p.team_id = tpr.team_id)
where p.id = $1 where p.id = ?
and tpr.profile_id = $2 and tpr.profile_id = ?
union all union all
select ppr.is_owner, select ppr.is_owner,
ppr.is_admin, ppr.is_admin,
ppr.can_edit ppr.can_edit
from project_profile_rel as ppr from project_profile_rel as ppr
where ppr.project_id = $1 where ppr.project_id = ?
and ppr.profile_id = $2") and ppr.profile_id = ?")
(defn check-edition-permissions! (defn check-edition-permissions!
[conn profile-id project-id] [conn profile-id project-id]
(-> (db/query conn [sql:project-permissions project-id profile-id]) (let [rows (db/exec! conn [sql:project-permissions
(p/then' seq) project-id profile-id
(p/then' su/raise-not-found-if-nil) project-id profile-id])]
(p/then' (fn [rows] (when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows) (when-not (or (some :can-edit rows)
(some :is-admin rows) (some :is-admin rows)
(some :is-owner rows)) (some :is-owner rows))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))
;; --- Mutation: Create Project ;; --- Mutation: Create Project
@ -72,30 +70,28 @@
(sm/defmutation ::create-project (sm/defmutation ::create-project
[params] [params]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [proj (create-project conn params)] (let [proj (create-project conn params)]
(create-project-profile conn (assoc params :project-id (:id proj))) (create-project-profile conn (assoc params :project-id (:id proj)))
proj))) proj)))
(def ^:private sql:insert-project
"insert into project (id, team_id, name, is_default)
values ($1, $2, $3, $4)
returning *")
(defn create-project (defn create-project
[conn {:keys [id profile-id team-id name default?] :as params}] [conn {:keys [id profile-id team-id name default?] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
default? (if (boolean? default?) default? false)] default? (if (boolean? default?) default? false)]
(db/query-one conn [sql:insert-project id team-id name default?]))) (db/insert! conn :project
{:id id
(def ^:private sql:create-project-profile :team-id team-id
"insert into project_profile_rel (project_id, profile_id, is_owner, is_admin, can_edit) :name name
values ($1, $2, true, true, true) :is-default default?})))
returning *")
(defn create-project-profile (defn create-project-profile
[conn {:keys [project-id profile-id] :as params}] [conn {:keys [project-id profile-id] :as params}]
(-> (db/query-one conn [sql:create-project-profile project-id profile-id]) (db/insert! conn :project-profile-rel
(p/then' su/constantly-nil))) {:project-id project-id
:profile-id profile-id
:is-owner true
:is-admin true
:can-edit true}))
@ -107,23 +103,13 @@
(s/keys :req-un [::profile-id ::name ::id])) (s/keys :req-un [::profile-id ::name ::id]))
(sm/defmutation ::rename-project (sm/defmutation ::rename-project
[{:keys [id profile-id] :as params}] [{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [project (db/get-by-id conn :project id {:for-update true})]
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(rename-project conn params))) (db/update! conn :project
{:name name}
(def ^:private sql:rename-project {:id id}))))
"update project
set name = $2
where id = $1
and deleted_at is null
returning *")
(defn rename-project
[conn {:keys [id name] :as params}]
(db/query-one conn [sql:rename-project id name]))
;; --- Mutation: Delete Project ;; --- Mutation: Delete Project
@ -147,10 +133,10 @@
(def ^:private sql:mark-project-deleted (def ^:private sql:mark-project-deleted
"update project "update project
set deleted_at = clock_timestamp() set deleted_at = clock_timestamp()
where id = $1 where id = ?
returning id") returning id")
(defn mark-project-deleted (defn mark-project-deleted
[conn {:keys [id profile-id] :as params}] [conn {:keys [id profile-id] :as params}]
(-> (db/query-one conn [sql:mark-project-deleted id]) (db/exec! conn [sql:mark-project-deleted id])
(p/then' su/constantly-nil))) nil)

View file

@ -10,14 +10,12 @@
(ns uxbox.services.mutations.teams (ns uxbox.services.mutations.teams
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.db :as db]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.util :as su] [uxbox.util.blob :as blob]))
[uxbox.util.blob :as blob]
[uxbox.common.uuid :as uuid]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -37,32 +35,28 @@
(sm/defmutation ::create-team (sm/defmutation ::create-team
[params] [params]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [team (create-team conn params)] (let [team (create-team conn params)]
(create-team-profile conn (assoc params :team-id (:id team))) (create-team-profile conn (assoc params :team-id (:id team)))
team))) team)))
(def ^:private sql:insert-team
"insert into team (id, name, photo, is_default)
values ($1, $2, '', $3)
returning *")
(def ^:private sql:create-team-profile
"insert into team_profile_rel (team_id, profile_id, is_owner, is_admin, can_edit)
values ($1, $2, true, true, true)
returning *")
(defn create-team (defn create-team
[conn {:keys [id profile-id name default?] :as params}] [conn {:keys [id profile-id name default?] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
default? (if (boolean? default?) default? false)] default? (if (boolean? default?) default? false)]
(db/query-one conn [sql:insert-team id name default?]))) (db/insert! conn :team
{:id id
:name name
:photo ""
:is-default default?})))
(defn create-team-profile (defn create-team-profile
[conn {:keys [team-id profile-id] :as params}] [conn {:keys [team-id profile-id] :as params}]
(-> (db/query-one conn [sql:create-team-profile team-id profile-id]) (db/insert! conn :team-profile-rel
(p/then' su/constantly-nil))) {:team-id team-id
:profile-id profile-id
:is-owner true
:is-admin true
:can-edit true}))
;; --- Mutation: Team Edition Permissions ;; --- Mutation: Team Edition Permissions
@ -71,18 +65,14 @@
tpr.is_admin, tpr.is_admin,
tpr.can_edit tpr.can_edit
from team_profile_rel as tpr from team_profile_rel as tpr
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.team_id = $2") and tpr.team_id = ?")
(defn check-edition-permissions! (defn check-edition-permissions!
[conn profile-id team-id] [conn profile-id team-id]
(-> (db/query-one conn [sql:team-permissions profile-id team-id]) (let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
(p/then' (fn [row]
(when-not (or (:can-edit row) (when-not (or (:can-edit row)
(:is-admin row) (:is-admin row)
(:is-owner row)) (:is-owner row))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))

View file

@ -1,48 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.mutations.user-attrs
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.common.spec :as us]
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.queries.user-attrs :refer [decode-row]]
[uxbox.util.blob :as blob]))
;; --- Update
(s/def ::user ::us/uuid)
(s/def ::key ::us/string)
(s/def ::val any?)
(s/def ::upsert-user-attr
(s/keys :req-un [::key ::val ::user]))
(sm/defmutation ::upsert-user-attr
[{:keys [key val user] :as params}]
(let [sql "insert into user_attrs (key, val, user_id)
values ($1, $2, $3)
on conflict (user_id, key)
do update set val = $2"
val (blob/encode val)]
(-> (db/query-one db/pool [sql key val user])
(p/then' su/constantly-nil))))
;; --- Delete KVStore
(s/def ::delete-user-attr
(s/keys :req-un [::key ::user]))
(sm/defmutation ::delete-user-attr
[{:keys [user key] :as params}]
(let [sql "delete from user_attrs
where user_id = $2
and key = $1"]
(-> (db/query-one db/pool [sql key user])
(p/then' su/constantly-nil))))

View file

@ -7,14 +7,29 @@
(ns uxbox.services.notifications (ns uxbox.services.notifications
"A websocket based notifications mechanism." "A websocket based notifications mechanism."
(:require (:require
[clojure.tools.logging :as log]
[clojure.core.async :as a :refer [>! <!]] [clojure.core.async :as a :refer [>! <!]]
[clojure.tools.logging :as log]
[promesa.core :as p] [promesa.core :as p]
[ring.adapter.jetty9 :as jetty]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.util.transit :as t]
[uxbox.redis :as redis]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[vertx.util :as vu :refer [<?]])) [uxbox.redis :as redis]
[ring.util.codec :as codec]
[uxbox.util.transit :as t]))
(defmacro go-try
[& body]
`(a/go
(try
~@body
(catch Throwable e# e#))))
(defmacro <?
[ch]
`(let [r# (a/<! ~ch)]
(if (instance? Throwable r#)
(throw r#)
r#)))
(defn- decode-message (defn- decode-message
[message] [message]
@ -30,14 +45,14 @@
(defn- publish (defn- publish
[channel message] [channel message]
(vu/go-try (go-try
(let [message (encode-message message)] (let [message (encode-message message)]
(<? (redis/run :publish {:channel (str channel) (<? (redis/run :publish {:channel (str channel)
:message message}))))) :message message})))))
(defn- retrieve-presence (defn- retrieve-presence
[key] [key]
(vu/go-try (go-try
(let [data (<? (redis/run :hgetall {:key key}))] (let [data (<? (redis/run :hgetall {:key key}))]
(into [] (map (fn [[k v]] [(uuid/uuid k) (uuid/uuid v)])) data)))) (into [] (map (fn [[k v]] [(uuid/uuid k) (uuid/uuid v)])) data))))
@ -46,7 +61,7 @@
(let [key (str file-id) (let [key (str file-id)
field (str session-id) field (str session-id)
value (str profile-id)] value (str profile-id)]
(vu/go-try (go-try
(<? (redis/run :hset {:key key :field field :value value})) (<? (redis/run :hset {:key key :field field :value value}))
(<? (retrieve-presence key))))) (<? (retrieve-presence key)))))
@ -54,7 +69,7 @@
[file-id session-id profile-id] [file-id session-id profile-id]
(let [key (str file-id) (let [key (str file-id)
field (str session-id)] field (str session-id)]
(vu/go-try (go-try
(<? (redis/run :hdel {:key key :field field})) (<? (redis/run :hdel {:key key :field field}))
(<? (retrieve-presence key))))) (<? (retrieve-presence key)))))
@ -69,14 +84,14 @@
(defmethod handle-message :connect (defmethod handle-message :connect
[{:keys [file-id profile-id session-id output] :as ws} message] [{:keys [file-id profile-id session-id output] :as ws} message]
(log/info (str "profile " profile-id " is connected to " file-id)) (log/info (str "profile " profile-id " is connected to " file-id))
(vu/go-try (go-try
(let [members (<? (join-room file-id session-id profile-id))] (let [members (<? (join-room file-id session-id profile-id))]
(<? (publish file-id {:type :presence :sessions members}))))) (<? (publish file-id {:type :presence :sessions members})))))
(defmethod handle-message :disconnect (defmethod handle-message :disconnect
[{:keys [profile-id file-id session-id] :as ws} message] [{:keys [profile-id file-id session-id] :as ws} message]
(log/info (str "profile " profile-id " is disconnected from " file-id)) (log/info (str "profile " profile-id " is disconnected from " file-id))
(vu/go-try (go-try
(let [members (<? (leave-room file-id session-id profile-id))] (let [members (<? (leave-room file-id session-id profile-id))]
(<? (publish file-id {:type :presence :sessions members}))))) (<? (publish file-id {:type :presence :sessions members})))))
@ -87,7 +102,7 @@
(defmethod handle-message :pointer-update (defmethod handle-message :pointer-update
[{:keys [profile-id file-id session-id] :as ws} message] [{:keys [profile-id file-id session-id] :as ws} message]
(vu/go-try (go-try
(let [message (assoc message (let [message (assoc message
:profile-id profile-id :profile-id profile-id
:session-id session-id)] :session-id session-id)]
@ -97,43 +112,31 @@
;; WebSocket Handler ;; WebSocket Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- process-message
[ws message]
(vu/go-try
(let [message (decode-message message)]
(<? (handle-message ws message)))))
(defn- forward-message (defn- forward-message
[{:keys [output session-id profile-id] :as ws} message] [{:keys [out session-id profile-id] :as ws} message]
(vu/go-try (go-try
(let [message' (decode-message message)] (when-not (= (:session-id message) session-id)
(when-not (= (:session-id message') session-id) (>! out message))))
(>! output message)))))
(defn- close-all!
[{:keys [sch] :as ws}]
(a/close! sch)
(.close ^java.lang.AutoCloseable ws))
(defn start-loop! (defn start-loop!
[{:keys [input output sch on-error] :as ws}] [{:keys [in out sub] :as ws}]
(vu/go-try (go-try
(loop [] (loop []
(let [timeout (a/timeout 30000) (let [timeout (a/timeout 30000)
[val port] (a/alts! [input sch timeout])] [val port] (a/alts! [in sub timeout])]
;; (prn "alts" val "from" (cond (= port input) "input" ;; (prn "alts" val "from" (cond (= port in) "input"
;; (= port sch) "redis" ;; (= port sub) "redis"
;; :else "timeout")) ;; :else "timeout"))
(cond (cond
;; Process message coming from connected client ;; Process message coming from connected client
(and (= port input) (not (nil? val))) (and (= port in) (not (nil? val)))
(do (do
(<? (process-message ws val)) (<? (handle-message ws val))
(recur)) (recur))
;; Forward message to the websocket ;; Forward message to the websocket
(and (= port sch) (not (nil? val))) (and (= port sub) (not (nil? val)))
(do (do
(<? (forward-message ws val)) (<? (forward-message ws val))
(recur)) (recur))
@ -141,36 +144,68 @@
;; Timeout channel signaling ;; Timeout channel signaling
(= port timeout) (= port timeout)
(do (do
(>! output (encode-message {:type :ping})) (>! out {:type :ping})
(recur)) (recur))
:else :else
nil))))) nil)))))
(defn disconnect!
[conn]
(.. conn (getSession) (disconnect)))
(defn- on-subscribed (defn- on-subscribed
[{:keys [on-error] :as ws} sch] [{:keys [conn] :as ws}]
(let [ws (assoc ws :sch sch)]
(a/go (a/go
(try (try
(<? (handle-message ws {:type :connect})) (<? (handle-message ws {:type :connect}))
(<? (start-loop! ws)) (<? (start-loop! ws))
(<? (handle-message ws {:type :disconnect})) (<? (handle-message ws {:type :disconnect}))
(close-all! ws) (catch Throwable err
(catch Throwable e (log/error "Unexpected exception on websocket handler:\n"
(on-error e) (with-out-str
(close-all! ws)))))) (.printStackTrace err (java.io.PrintWriter. *out*))))
(disconnect! conn)))))
(defrecord WebSocket [conn in out sub])
(defn- start-rcv-loop!
[{:keys [conn out] :as ws}]
(a/go-loop []
(let [val (a/<! out)]
(when-not (nil? val)
(jetty/send! conn (encode-message val))
(recur)))))
(defn websocket (defn websocket
[req {:keys [input on-error] :as ws}] [{:keys [file-id] :as params}]
(let [fid (uuid/uuid (get-in req [:path-params :file-id])) (let [in (a/chan 32)
sid (uuid/uuid (get-in req [:path-params :session-id])) out (a/chan 32)]
pid (:profile-id req) {:on-connect (fn [conn]
ws (assoc ws (let [xf (map decode-message)
:profile-id pid sub (redis/subscribe (str file-id) xf)
:file-id fid ws (WebSocket. conn in out sub nil params)]
:session-id sid)] (start-rcv-loop! ws)
(-> (redis/subscribe (str fid)) (a/go
(p/finally (fn [sch error] (a/<! (on-subscribed ws))
(if error (a/close! sub))))
(on-error error)
(on-subscribed ws sch))))))) :on-error (fn [conn e]
;; (prn "websocket" :on-error e)
(a/close! out)
(a/close! in))
:on-close (fn [conn status-code reason]
;; (prn "websocket" :on-close status-code reason)
(a/close! out)
(a/close! in))
:on-text (fn [ws message]
(let [message (decode-message message)]
;; (prn "websocket" :on-text message)
(a/>!! in message)))
:on-bytes (fn [ws bytes offset len]
#_(prn "websocket" :on-bytes bytes))}))

View file

@ -14,16 +14,14 @@
[promesa.exec :as px] [promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db]
[uxbox.media :as media]
[uxbox.images :as images]
[uxbox.services.queries.teams :as teams]
[uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[vertx.core :as vc])) [uxbox.db :as db]
[uxbox.images :as images]
[uxbox.media :as media]
[uxbox.services.queries :as sq]
[uxbox.services.queries.teams :as teams]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -32,15 +30,13 @@
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::library-id (s/nilable ::us/uuid)) (s/def ::library-id (s/nilable ::us/uuid))
;; --- Query: Colors Librarys ;; --- Query: Colors Librarys
(def ^:private sql:libraries (def ^:private sql:libraries
"select lib.*, "select lib.*,
(select count(*) from color where library_id = lib.id) as num_colors (select count(*) from color where library_id = lib.id) as num_colors
from color_library as lib from color_library as lib
where lib.team_id = $1 where lib.team_id = ?
and lib.deleted_at is null and lib.deleted_at is null
order by lib.created_at desc") order by lib.created_at desc")
@ -51,8 +47,7 @@
[{:keys [profile-id team-id]}] [{:keys [profile-id team-id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(db/query conn [sql:libraries team-id]))) (db/exec! conn [sql:libraries team-id])))
;; --- Query: Color Library ;; --- Query: Color Library
@ -65,7 +60,7 @@
(sq/defquery ::color-library (sq/defquery ::color-library
[{:keys [profile-id id]}] [{:keys [profile-id id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn id)] (let [lib (retrieve-library conn id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
lib))) lib)))
@ -74,14 +69,14 @@
(select count(*) from color where library_id = lib.id) as num_colors (select count(*) from color where library_id = lib.id) as num_colors
from color_library as lib from color_library as lib
where lib.deleted_at is null where lib.deleted_at is null
and lib.id = $1") and lib.id = ?")
(defn- retrieve-library (defn- retrieve-library
[conn id] [conn id]
(-> (db/query-one conn [sql:single-library id]) (let [row (db/exec-one! conn [sql:single-library id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))
;; --- Query: Colors (by library) ;; --- Query: Colors (by library)
@ -93,7 +88,7 @@
(sq/defquery ::colors (sq/defquery ::colors
[{:keys [profile-id library-id] :as params}] [{:keys [profile-id library-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn library-id)] (let [lib (retrieve-library conn library-id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
(retrieve-colors conn library-id)))) (retrieve-colors conn library-id))))
@ -102,13 +97,12 @@
from color as color from color as color
inner join color_library as lib on (lib.id = color.library_id) inner join color_library as lib on (lib.id = color.library_id)
where color.deleted_at is null where color.deleted_at is null
and color.library_id = $1 and color.library_id = ?
order by created_at desc") order by created_at desc")
(defn- retrieve-colors (defn- retrieve-colors
[conn library-id] [conn library-id]
(db/query conn [sql:colors library-id])) (db/exec! conn [sql:colors library-id]))
;; --- Query: Color (by ID) ;; --- Query: Color (by ID)
@ -122,7 +116,7 @@
(sq/defquery ::color (sq/defquery ::color
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [color (retrieve-color conn id)] (let [color (retrieve-color conn id)]
(teams/check-read-permissions! conn profile-id (:team-id color)) (teams/check-read-permissions! conn profile-id (:team-id color))
color))) color)))
@ -132,10 +126,12 @@
from color as color from color as color
inner join color_library as lib on (lib.id = color.library_id) inner join color_library as lib on (lib.id = color.library_id)
where color.deleted_at is null where color.deleted_at is null
and color.id = $1 and color.id = ?
order by created_at desc") order by created_at desc")
(defn retrieve-color (defn retrieve-color
[conn id] [conn id]
(-> (db/query-one conn [sql:single-color id]) (let [row (db/exec-one! conn [sql:single-color id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))

View file

@ -16,7 +16,6 @@
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob])) [uxbox.util.blob :as blob]))
(declare decode-row) (declare decode-row)
@ -38,8 +37,8 @@
select p.* select p.*
from project as p from project as p
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id) inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
where tpr.profile_id = $1 where tpr.profile_id = ?
and p.team_id = $2 and p.team_id = ?
and p.deleted_at is null and p.deleted_at is null
and (tpr.is_admin = true or and (tpr.is_admin = true or
tpr.is_owner = true or tpr.is_owner = true or
@ -48,8 +47,8 @@
select p.* select p.*
from project as p from project as p
inner join project_profile_rel as ppr on (ppr.project_id = p.id) inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = $1 where ppr.profile_id = ?
and p.team_id = $2 and p.team_id = ?
and p.deleted_at is null and p.deleted_at is null
and (ppr.is_admin = true or and (ppr.is_admin = true or
ppr.is_owner = true or ppr.is_owner = true or
@ -62,7 +61,7 @@
from file from file
inner join projects as pr on (file.project_id = pr.id) inner join projects as pr on (file.project_id = pr.id)
left join page on (file.id = page.file_id) left join page on (file.id = page.file_id)
where file.name ilike ('%' || $3 || '%') where file.name ilike ('%' || ? || '%')
window pages_w as (partition by file.id order by page.created_at window pages_w as (partition by file.id order by page.created_at
range between unbounded preceding range between unbounded preceding
and unbounded following) and unbounded following)
@ -73,8 +72,12 @@
(sq/defquery ::search-files (sq/defquery ::search-files
[{:keys [profile-id team-id search-term] :as params}] [{:keys [profile-id team-id search-term] :as params}]
(-> (db/query db/pool [sql:search-files profile-id team-id search-term]) (let [rows (db/exec! db/pool [sql:search-files
(p/then (partial mapv decode-row)))) profile-id team-id
profile-id team-id
search-term])]
(mapv decode-row rows)))
;; --- Query: Draft Files ;; --- Query: Draft Files
@ -86,8 +89,8 @@
from file as f from file as f
inner join file_profile_rel as fp_r on (fp_r.file_id = f.id) inner join file_profile_rel as fp_r on (fp_r.file_id = f.id)
left join page as pg on (f.id = pg.file_id) left join page as pg on (f.id = pg.file_id)
where fp_r.profile_id = $1 where fp_r.profile_id = ?
and f.project_id = $2 and f.project_id = ?
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null and pg.deleted_at is null
and (fp_r.is_admin = true or and (fp_r.is_admin = true or
@ -104,8 +107,8 @@
(sq/defquery ::files (sq/defquery ::files
[{:keys [profile-id project-id] :as params}] [{:keys [profile-id project-id] :as params}]
(-> (db/query db/pool [sql:files profile-id project-id]) (->> (db/exec! db/pool [sql:files profile-id project-id])
(p/then (partial mapv decode-row)))) (mapv decode-row)))
;; --- Query: File Permissions ;; --- Query: File Permissions
@ -114,8 +117,8 @@
fpr.is_admin, fpr.is_admin,
fpr.can_edit fpr.can_edit
from file_profile_rel as fpr from file_profile_rel as fpr
where fpr.file_id = $1 where fpr.file_id = ?
and fpr.profile_id = $2 and fpr.profile_id = ?
union all union all
select tpr.is_owner, select tpr.is_owner,
tpr.is_admin, tpr.is_admin,
@ -123,28 +126,31 @@
from team_profile_rel as tpr from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id) inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id) inner join file as f on (p.id = f.project_id)
where f.id = $1 where f.id = ?
and tpr.profile_id = $2 and tpr.profile_id = ?
union all union all
select ppr.is_owner, select ppr.is_owner,
ppr.is_admin, ppr.is_admin,
ppr.can_edit ppr.can_edit
from project_profile_rel as ppr from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id) inner join file as f on (f.project_id = ppr.project_id)
where f.id = $1 where f.id = ?
and ppr.profile_id = $2;") and ppr.profile_id = ?;")
(defn check-edition-permissions! (defn check-edition-permissions!
[conn profile-id file-id] [conn profile-id file-id]
(-> (db/query conn [sql:file-permissions file-id profile-id]) (let [rows (db/exec! conn [sql:file-permissions
(p/then' seq) file-id profile-id
(p/then' su/raise-not-found-if-nil) file-id profile-id
(p/then' (fn [rows] file-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows) (when-not (or (some :can-edit rows)
(some :is-admin rows) (some :is-admin rows)
(some :is-owner rows)) (some :is-owner rows))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))
;; --- Query: Images of the File ;; --- Query: Images of the File
@ -162,15 +168,15 @@
(def ^:private sql:file-images (def ^:private sql:file-images
"select fi.* "select fi.*
from file_image as fi from file_image as fi
where fi.file_id = $1") where fi.file_id = ?")
(defn retrieve-file-images (defn retrieve-file-images
[conn {:keys [file-id] :as params}] [conn {:keys [file-id] :as params}]
(let [sqlv [sql:file-images file-id] (let [sqlv [sql:file-images file-id]
xf (comp (map #(images/resolve-urls % :path :uri)) xf (comp (map #(images/resolve-urls % :path :uri))
(map #(images/resolve-urls % :thumb-path :thumb-uri)))] (map #(images/resolve-urls % :thumb-path :thumb-uri)))]
(-> (db/query conn sqlv) (->> (db/exec! conn sqlv)
(p/then' #(into [] xf %))))) (into [] xf))))
;; --- Query: File (By ID) ;; --- Query: File (By ID)
@ -179,7 +185,7 @@
array_agg(pg.id) over pages_w as pages array_agg(pg.id) over pages_w as pages
from file as f from file as f
left join page as pg on (f.id = pg.file_id) left join page as pg on (f.id = pg.file_id)
where f.id = $1 where f.id = ?
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null and pg.deleted_at is null
window pages_w as (partition by f.id order by pg.ordering window pages_w as (partition by f.id order by pg.ordering
@ -190,27 +196,26 @@
"select pf.id, pf.fullname, pf.photo "select pf.id, pf.fullname, pf.photo
from profile as pf from profile as pf
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
where fpr.file_id = $1 where fpr.file_id = ?
union union
select pf.id, pf.fullname, pf.photo select pf.id, pf.fullname, pf.photo
from profile as pf from profile as pf
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
inner join project as p on (tpr.team_id = p.team_id) inner join project as p on (tpr.team_id = p.team_id)
inner join file as f on (p.id = f.project_id) inner join file as f on (p.id = f.project_id)
where f.id = $1") where f.id = ?")
(defn retrieve-file (defn retrieve-file
[conn id] [conn id]
(-> (db/query-one conn [sql:file id]) (let [row (db/exec-one! conn [sql:file id])]
(p/then' su/raise-not-found-if-nil) (when-not row
(p/then' decode-row))) (ex/raise :type :not-found))
(decode-row row)))
(defn retrieve-file-users (defn retrieve-file-users
[conn id] [conn id]
(-> (db/query conn [sql:file-users id]) (->> (db/exec! conn [sql:file-users id id])
(p/then (fn [rows] (mapv #(images/resolve-media-uris % [:photo :photo-uri]))))
(mapv #(images/resolve-media-uris % [:photo :photo-uri]) rows)))))
(s/def ::file-users (s/def ::file-users
(s/keys :req-un [::profile-id ::id])) (s/keys :req-un [::profile-id ::id]))
@ -230,7 +235,6 @@
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(retrieve-file conn id))) (retrieve-file conn id)))
;; --- Helpers ;; --- Helpers
(defn decode-row (defn decode-row
@ -238,4 +242,4 @@
(when row (when row
(cond-> row (cond-> row
data (assoc :data (blob/decode data)) data (assoc :data (blob/decode data))
pages (assoc :pages (vec (remove nil? pages)))))) pages (assoc :pages (vec (.getArray pages))))))

View file

@ -14,16 +14,14 @@
[promesa.exec :as px] [promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db]
[uxbox.media :as media]
[uxbox.images :as images]
[uxbox.services.queries.teams :as teams]
[uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[vertx.core :as vc])) [uxbox.db :as db]
[uxbox.images :as images]
[uxbox.media :as media]
[uxbox.services.queries :as sq]
[uxbox.services.queries.teams :as teams]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -39,15 +37,13 @@
(cond-> row (cond-> row
metadata (assoc :metadata (blob/decode metadata))))) metadata (assoc :metadata (blob/decode metadata)))))
;; --- Query: Icons Librarys ;; --- Query: Icons Librarys
(def ^:private sql:libraries (def ^:private sql:libraries
"select lib.*, "select lib.*,
(select count(*) from icon where library_id = lib.id) as num_icons (select count(*) from icon where library_id = lib.id) as num_icons
from icon_library as lib from icon_library as lib
where lib.team_id = $1 where lib.team_id = ?
and lib.deleted_at is null and lib.deleted_at is null
order by lib.created_at desc") order by lib.created_at desc")
@ -58,7 +54,7 @@
[{:keys [profile-id team-id]}] [{:keys [profile-id team-id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(db/query conn [sql:libraries team-id]))) (db/exec! conn [sql:libraries team-id])))
@ -72,7 +68,7 @@
(sq/defquery ::icon-library (sq/defquery ::icon-library
[{:keys [profile-id id]}] [{:keys [profile-id id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn id)] (let [lib (retrieve-library conn id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
lib))) lib)))
@ -81,12 +77,14 @@
(select count(*) from icon where library_id = lib.id) as num_icons (select count(*) from icon where library_id = lib.id) as num_icons
from icon_library as lib from icon_library as lib
where lib.deleted_at is null where lib.deleted_at is null
and lib.id = $1") and lib.id = ?")
(defn- retrieve-library (defn- retrieve-library
[conn id] [conn id]
(-> (db/query-one conn [sql:single-library id]) (let [row (db/exec-one! conn [sql:single-library id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))
@ -100,22 +98,22 @@
(sq/defquery ::icons (sq/defquery ::icons
[{:keys [profile-id library-id] :as params}] [{:keys [profile-id library-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn library-id)] (let [lib (retrieve-library conn library-id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
(-> (retrieve-icons conn library-id) (->> (retrieve-icons conn library-id)
(p/then' (fn [rows] (mapv decode-row rows))))))) (mapv decode-row)))))
(def ^:private sql:icons (def ^:private sql:icons
"select icon.* "select icon.*
from icon as icon from icon as icon
inner join icon_library as lib on (lib.id = icon.library_id) inner join icon_library as lib on (lib.id = icon.library_id)
where icon.deleted_at is null where icon.deleted_at is null
and icon.library_id = $1 and icon.library_id = ?
order by created_at desc") order by created_at desc")
(defn- retrieve-icons (defn- retrieve-icons
[conn library-id] [conn library-id]
(db/query conn [sql:icons library-id])) (db/exec! conn [sql:icons library-id]))
@ -130,7 +128,7 @@
(sq/defquery ::icon (sq/defquery ::icon
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [icon (retrieve-icon conn id)] (let [icon (retrieve-icon conn id)]
(teams/check-read-permissions! conn profile-id (:team-id icon)) (teams/check-read-permissions! conn profile-id (:team-id icon))
(decode-row icon)))) (decode-row icon))))
@ -140,11 +138,13 @@
from icon as icon from icon as icon
inner join icon_library as lib on (lib.id = icon.library_id) inner join icon_library as lib on (lib.id = icon.library_id)
where icon.deleted_at is null where icon.deleted_at is null
and icon.id = $1 and icon.id = ?
order by created_at desc") order by created_at desc")
(defn retrieve-icon (defn retrieve-icon
[conn id] [conn id]
(-> (db/query-one conn [sql:single-icon id]) (let [row (db/exec-one! conn [sql:single-icon id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))

View file

@ -11,12 +11,12 @@
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.services.queries.teams :as teams] [uxbox.services.queries.teams :as teams]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]))
[uxbox.services.util :as su]))
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
@ -30,7 +30,7 @@
"select lib.*, "select lib.*,
(select count(*) from image where library_id = lib.id) as num_images (select count(*) from image where library_id = lib.id) as num_images
from image_library as lib from image_library as lib
where lib.team_id = $1 where lib.team_id = ?
and lib.deleted_at is null and lib.deleted_at is null
order by lib.created_at desc") order by lib.created_at desc")
@ -41,7 +41,7 @@
[{:keys [profile-id team-id]}] [{:keys [profile-id team-id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(db/query conn [sql:libraries team-id]))) (db/exec! conn [sql:libraries team-id])))
;; --- Query: Image Library ;; --- Query: Image Library
@ -54,7 +54,7 @@
(sq/defquery ::image-library (sq/defquery ::image-library
[{:keys [profile-id id]}] [{:keys [profile-id id]}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn id)] (let [lib (retrieve-library conn id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
lib))) lib)))
@ -63,13 +63,14 @@
(select count(*) from image where library_id = lib.id) as num_images (select count(*) from image where library_id = lib.id) as num_images
from image_library as lib from image_library as lib
where lib.deleted_at is null where lib.deleted_at is null
and lib.id = $1") and lib.id = ?")
(defn- retrieve-library (defn- retrieve-library
[conn id] [conn id]
(-> (db/query-one conn [sql:single-library id]) (let [row (db/exec-one! conn [sql:single-library id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))
;; --- Query: Images (by library) ;; --- Query: Images (by library)
@ -85,13 +86,11 @@
(sq/defquery ::images (sq/defquery ::images
[{:keys [profile-id library-id] :as params}] [{:keys [profile-id library-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [lib (retrieve-library conn library-id)] (let [lib (retrieve-library conn library-id)]
(teams/check-read-permissions! conn profile-id (:team-id lib)) (teams/check-read-permissions! conn profile-id (:team-id lib))
(-> (retrieve-images conn library-id) (->> (retrieve-images conn library-id)
(p/then' (fn [rows]
(->> rows
(mapv #(images/resolve-urls % :path :uri)) (mapv #(images/resolve-urls % :path :uri))
(mapv #(images/resolve-urls % :thumb-path :thumb-uri))))))))) (mapv #(images/resolve-urls % :thumb-path :thumb-uri))))))
(def ^:private sql:images (def ^:private sql:images
@ -99,12 +98,12 @@
from image as img from image as img
inner join image_library as lib on (lib.id = img.library_id) inner join image_library as lib on (lib.id = img.library_id)
where img.deleted_at is null where img.deleted_at is null
and img.library_id = $1 and img.library_id = ?
order by created_at desc") order by created_at desc")
(defn- retrieve-images (defn- retrieve-images
[conn library-id] [conn library-id]
(db/query conn [sql:images library-id])) (db/exec! conn [sql:images library-id]))
@ -119,7 +118,7 @@
(sq/defquery ::image (sq/defquery ::image
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [img (retrieve-image conn id)] (let [img (retrieve-image conn id)]
(teams/check-read-permissions! conn profile-id (:team-id img)) (teams/check-read-permissions! conn profile-id (:team-id img))
(-> img (-> img
(images/resolve-urls :path :uri) (images/resolve-urls :path :uri)
@ -131,13 +130,14 @@
from image as img from image as img
inner join image_library as lib on (lib.id = img.library_id) inner join image_library as lib on (lib.id = img.library_id)
where img.deleted_at is null where img.deleted_at is null
and img.id = $1 and img.id = ?
order by created_at desc") order by created_at desc")
(defn retrieve-image (defn retrieve-image
[conn id] [conn id]
(-> (db/query-one conn [sql:single-image id]) (let [row (db/exec-one! conn [sql:single-image id])]
(p/then' su/raise-not-found-if-nil))) (when-not row
(ex/raise :type :not-found))
row))

View file

@ -12,12 +12,11 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.exceptions :as ex]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.services.queries.files :as files] [uxbox.services.queries.files :as files]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]))
[uxbox.util.sql :as sql]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -28,8 +27,6 @@
(s/def ::project-id ::us/uuid) (s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
;; --- Query: Pages (By File ID) ;; --- Query: Pages (By File ID)
(declare retrieve-pages) (declare retrieve-pages)
@ -46,16 +43,14 @@
(def ^:private sql:pages (def ^:private sql:pages
"select p.* "select p.*
from page as p from page as p
where p.file_id = $1 where p.file_id = ?
and p.deleted_at is null and p.deleted_at is null
order by p.created_at asc") order by p.created_at asc")
(defn- retrieve-pages (defn- retrieve-pages
[conn {:keys [profile-id file-id] :as params}] [conn {:keys [profile-id file-id] :as params}]
(-> (db/query conn [sql:pages file-id]) (->> (db/exec! conn [sql:pages file-id])
(p/then (partial mapv decode-row)))) (mapv decode-row)))
;; --- Query: Single Page (By ID) ;; --- Query: Single Page (By ID)
@ -66,20 +61,20 @@
(sq/defquery ::page (sq/defquery ::page
[{:keys [profile-id id] :as params}] [{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool] (with-open [conn (db/open)]
(p/let [page (retrieve-page conn id)] (let [page (retrieve-page conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page)) (files/check-edition-permissions! conn profile-id (:file-id page))
page))) page)))
(def ^:private sql:page (def ^:private sql:page
"select p.* from page as p where id=$1") "select p.* from page as p where id=?")
(defn retrieve-page (defn retrieve-page
[conn id] [conn id]
(-> (db/query-one conn [sql:page id]) (let [row (db/exec-one! conn [sql:page id])]
(p/then' su/raise-not-found-if-nil) (when-not row
(p/then' decode-row))) (ex/raise :type :not-found))
(decode-row row)))
;; --- Query: Page Changes ;; --- Query: Page Changes
@ -90,10 +85,10 @@
pc.changes, pc.changes,
pc.revn pc.revn
from page_change as pc from page_change as pc
where pc.page_id=$1 where pc.page_id=?
order by pc.revn asc order by pc.revn asc
limit $2 limit ?
offset $3") offset ?")
(s/def ::skip ::us/integer) (s/def ::skip ::us/integer)
@ -104,14 +99,14 @@
(defn retrieve-page-changes (defn retrieve-page-changes
[conn id skip limit] [conn id skip limit]
(-> (db/query conn [sql:page-changes id limit skip]) (->> (db/exec! conn [sql:page-changes id limit skip])
(p/then' #(mapv decode-row %)))) (mapv decode-row)))
(sq/defquery ::page-changes (sq/defquery ::page-changes
[{:keys [profile-id id skip limit]}] [{:keys [profile-id id skip limit]}]
(when *assert* (when *assert*
(-> (db/query db/pool [sql:page-changes id limit skip]) (-> (db/exec! db/pool [sql:page-changes id limit skip])
(p/then' #(mapv decode-row %))))) (mapv decode-row))))
;; --- Helpers ;; --- Helpers

View file

@ -7,14 +7,11 @@
(ns uxbox.services.queries.profile (ns uxbox.services.queries.profile
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.images :as images] [uxbox.images :as images]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[uxbox.util.blob :as blob])) [uxbox.util.blob :as blob]))
@ -43,7 +40,7 @@
(sq/defquery ::profile (sq/defquery ::profile
[{:keys [profile-id] :as params}] [{:keys [profile-id] :as params}]
(if profile-id (if profile-id
(db/with-atomic [conn db/pool] (with-open [conn (db/open)]
(retrieve-profile conn profile-id)) (retrieve-profile conn profile-id))
{:id uuid/zero {:id uuid/zero
:fullname "Anonymous User"})) :fullname "Anonymous User"}))
@ -57,41 +54,43 @@
"select t.id "select t.id
from team as t from team as t
inner join team_profile_rel as tpr on (tpr.team_id = t.id) inner join team_profile_rel as tpr on (tpr.team_id = t.id)
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.is_owner is true and tpr.is_owner is true
and t.is_default is true and t.is_default is true
union all union all
select p.id select p.id
from project as p from project as p
inner join project_profile_rel as tpr on (tpr.project_id = p.id) inner join project_profile_rel as tpr on (tpr.project_id = p.id)
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.is_owner is true and tpr.is_owner is true
and p.is_default is true") and p.is_default is true")
(defn retrieve-additional-data (defn retrieve-additional-data
[conn id] [conn id]
(-> (db/query conn [sql:default-team-and-project id]) (let [[team project] (db/exec! conn [sql:default-team-and-project id id])]
(p/then' (fn [[team project]]
{:default-team-id (:id team) {:default-team-id (:id team)
:default-project-id (:id project)})))) :default-project-id (:id project)}))
(defn retrieve-profile-data (defn retrieve-profile-data
[conn id] [conn id]
(let [sql "select * from profile where id=$1 and deleted_at is null"] (let [sql "select * from profile where id=? and deleted_at is null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defn retrieve-profile (defn retrieve-profile
[conn id] [conn id]
(p/let [prof (-> (retrieve-profile-data conn id) (let [profile (some-> (retrieve-profile-data conn id)
(p/then' su/raise-not-found-if-nil) (images/resolve-urls :photo :photo-uri)
(p/then' strip-private-attrs) (strip-private-attrs)
(p/then' #(images/resolve-media-uris % [:photo :photo-uri]))) (merge (retrieve-additional-data conn id)))]
addt (retrieve-additional-data conn id)] (when (nil? profile)
(merge prof addt))) (ex/raise :type :not-found
:hint "Object doest not exists."))
profile))
;; --- Attrs Helpers ;; --- Attrs Helpers
(defn strip-private-attrs (defn strip-private-attrs
"Only selects a publicy visible profile attrs." "Only selects a publicy visible profile attrs."
[profile] [o]
(select-keys profile [:id :fullname :lang :email :created-at :photo :theme :photo-uri])) (select-keys o [:id :fullname :lang :email :created-at :photo :theme :photo-uri]))

View file

@ -11,7 +11,6 @@
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob])) [uxbox.util.blob :as blob]))
(declare decode-row) (declare decode-row)
@ -32,7 +31,7 @@
and deleted_at is null) as file_count and deleted_at is null) as file_count
from project as p from project as p
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id) inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
where tpr.profile_id = $1 where tpr.profile_id = ?
and p.deleted_at is null and p.deleted_at is null
and (tpr.is_admin = true or and (tpr.is_admin = true or
tpr.is_owner = true or tpr.is_owner = true or
@ -44,7 +43,7 @@
and deleted_at is null) and deleted_at is null)
from project as p from project as p
inner join project_profile_rel as ppr on (ppr.project_id = p.id) inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = $1 where ppr.profile_id = ?
and p.deleted_at is null and p.deleted_at is null
and (ppr.is_admin = true or and (ppr.is_admin = true or
ppr.is_owner = true or ppr.is_owner = true or
@ -52,15 +51,15 @@
) )
select * select *
from projects from projects
where team_id = $2 where team_id = ?
order by modified_at desc") order by modified_at desc")
(def ^:private sql:project-by-id (def ^:private sql:project-by-id
"select p.* "select p.*
from project as p from project as p
inner join project_profile_rel as ppr on (ppr.project_id = p.id) inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = $1 where ppr.profile_id = ?
and p.id = $2 and p.id = ?
and p.deleted_at is null and p.deleted_at is null
and (ppr.is_admin = true or and (ppr.is_admin = true or
ppr.is_owner = true or ppr.is_owner = true or
@ -78,11 +77,11 @@
(defn retrieve-projects (defn retrieve-projects
[conn profile-id team-id] [conn profile-id team-id]
(db/query conn [sql:projects profile-id team-id])) (db/exec! conn [sql:projects profile-id profile-id team-id]))
(defn retrieve-project (defn retrieve-project
[conn profile-id id] [conn profile-id id]
(db/query-one conn [sql:project-by-id profile-id id])) (db/exec-one! conn [sql:project-by-id profile-id id]))
(sq/defquery ::projects-by-team (sq/defquery ::projects-by-team
[{:keys [profile-id team-id]}] [{:keys [profile-id team-id]}]

View file

@ -25,8 +25,8 @@
from file as f from file as f
inner join file_profile_rel as fp_r on (fp_r.file_id = f.id) inner join file_profile_rel as fp_r on (fp_r.file_id = f.id)
left join page as pg on (f.id = pg.file_id) left join page as pg on (f.id = pg.file_id)
where fp_r.profile_id = $1 where fp_r.profile_id = ?
and f.project_id = $2 and f.project_id = ?
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null and pg.deleted_at is null
and (fp_r.is_admin = true or and (fp_r.is_admin = true or
@ -38,10 +38,11 @@
order by f.modified_at desc order by f.modified_at desc
limit 5") limit 5")
(defn recent-by-project [profile-id project] (defn recent-by-project
[profile-id project]
(let [project-id (:id project)] (let [project-id (:id project)]
(-> (db/query db/pool [sql:project-files-recent profile-id project-id]) (->> (db/exec! db/pool [sql:project-files-recent profile-id project-id])
(p/then (partial mapv decode-row))))) (mapv decode-row))))
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid) (s/def ::profile-id ::us/uuid)
@ -51,8 +52,9 @@
(sq/defquery ::recent-files (sq/defquery ::recent-files
[{:keys [profile-id team-id]}] [{:keys [profile-id team-id]}]
(-> (retrieve-projects db/pool profile-id team-id) (->> (retrieve-projects db/pool profile-id team-id)
;; Retrieve for each proyect the 5 more recent files ;; Retrieve for each proyect the 5 more recent files
(p/then #(p/all (map (partial recent-by-project profile-id) %))) (map (partial recent-by-project profile-id))
;; Change the structure so it's a map with project-id as keys ;; Change the structure so it's a map with project-id as keys
(p/then #(->> % (flatten) (group-by :project-id))))) (flatten)
(group-by :project-id)))

View file

@ -10,15 +10,12 @@
(ns uxbox.services.queries.teams (ns uxbox.services.queries.teams
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.db :as db]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.services.util :as su] [uxbox.util.blob :as blob]))
[uxbox.util.blob :as blob]
[uxbox.common.uuid :as uuid]))
;; --- Team Edition Permissions ;; --- Team Edition Permissions
@ -27,27 +24,25 @@
tpr.is_admin, tpr.is_admin,
tpr.can_edit tpr.can_edit
from team_profile_rel as tpr from team_profile_rel as tpr
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.team_id = $2") and tpr.team_id = ?")
(defn check-edition-permissions! (defn check-edition-permissions!
[conn profile-id team-id] [conn profile-id team-id]
(-> (db/query-one conn [sql:team-permissions profile-id team-id]) (let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
(p/then' (fn [row]
(when-not (or (:can-edit row) (when-not (or (:can-edit row)
(:is-admin row) (:is-admin row)
(:is-owner row)) (:is-owner row))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))
(defn check-read-permissions! (defn check-read-permissions!
[conn profile-id team-id] [conn profile-id team-id]
(-> (db/query-one conn [sql:team-permissions profile-id team-id]) (let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
(p/then' (fn [row]
(when-not (or (:can-edit row) (when-not (or (:can-edit row)
(:is-admin row) (:is-admin row)
(:is-owner row) (:is-owner row)
;; We can read global-project owned items ;; We can read global-project owned items
(= team-id #uuid "00000000-0000-0000-0000-000000000000")) (= team-id #uuid "00000000-0000-0000-0000-000000000000"))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))

View file

@ -1,37 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.queries.user-attrs
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[uxbox.common.spec :as us]
[uxbox.db :as db]
[uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]))
(defn decode-row
[{:keys [val] :as row}]
(when row
(cond-> row
val (assoc :val (blob/decode val)))))
(s/def ::key ::us/string)
(s/def ::user ::us/uuid)
(s/def ::user-attr
(s/keys :req-un [::key ::user]))
(sq/defquery ::user-attr
[{:keys [key user]}]
(let [sql "select kv.*
from user_attrs as kv
where kv.user_id = $2
and kv.key = $1"]
(-> (db/query-one db/pool [sql key user])
(p/then' su/raise-not-found-if-nil)
(p/then' decode-row))))

View file

@ -14,17 +14,15 @@
[promesa.exec :as px] [promesa.exec :as px]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db]
[uxbox.media :as media]
[uxbox.images :as images]
[uxbox.services.queries.pages :as pages]
[uxbox.services.queries.files :as files]
[uxbox.services.queries :as sq]
[uxbox.services.util :as su]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[vertx.core :as vc])) [uxbox.db :as db]
[uxbox.images :as images]
[uxbox.media :as media]
[uxbox.services.queries :as sq]
[uxbox.services.queries.files :as files]
[uxbox.services.queries.pages :as pages]
[uxbox.util.blob :as blob]
[uxbox.util.data :as data]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -37,12 +35,12 @@
sql:project sql:project
"select p.id, p.name "select p.id, p.name
from project as p from project as p
where p.id = $1 where p.id = ?
and p.deleted_at is null") and p.deleted_at is null")
(defn- retrieve-project (defn- retrieve-project
[conn id] [conn id]
(db/query-one conn [sql:project id])) (db/exec-one! conn [sql:project id]))
(s/def ::share-token ::us/string) (s/def ::share-token ::us/string)
(s/def ::viewer-bundle (s/def ::viewer-bundle
@ -52,7 +50,7 @@
(sq/defquery ::viewer-bundle (sq/defquery ::viewer-bundle
[{:keys [profile-id page-id share-token] :as params}] [{:keys [profile-id page-id share-token] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(p/let [page (pages/retrieve-page conn page-id) (let [page (pages/retrieve-page conn page-id)
file (files/retrieve-file conn (:file-id page)) file (files/retrieve-file conn (:file-id page))
images (files/retrieve-file-images conn page) images (files/retrieve-file-images conn page)
project (retrieve-project conn (:project-id file))] project (retrieve-project conn (:project-id file))]

View file

@ -1,29 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.services.util
(:require
[clojure.tools.logging :as log]
[cuerdas.core :as str]
[vertx.util :as vu]
[uxbox.core :refer [system]]
[uxbox.common.exceptions :as ex]
[uxbox.common.uuid :as uuid]
[uxbox.util.dispatcher :as uds]))
(defn raise-not-found-if-nil
[v]
(if (nil? v)
(ex/raise :type :not-found
:hint "Object doest not exists.")
v))
(def constantly-nil (constantly nil))
(defn handle-on-context
[p]
(->> (vu/current-context system)
(vu/handle-on-context p)))

View file

@ -13,19 +13,15 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[promesa.core :as p]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.core :refer [system]]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.tasks.sendmail] [uxbox.tasks.sendmail]
[uxbox.tasks.remove-media] [uxbox.tasks.remove-media]
[uxbox.tasks.delete-profile] [uxbox.tasks.delete-profile]
[uxbox.tasks.delete-object] [uxbox.tasks.delete-object]
[uxbox.tasks.impl :as impl] [uxbox.tasks.impl :as impl]
[uxbox.util.time :as dt] [uxbox.util.time :as dt]))
[vertx.core :as vc]
[vertx.timers :as vt]))
;; --- Public API ;; --- Public API
@ -48,10 +44,17 @@
"remove-media" #'uxbox.tasks.remove-media/handler "remove-media" #'uxbox.tasks.remove-media/handler
"sendmail" #'uxbox.tasks.sendmail/handler}) "sendmail" #'uxbox.tasks.sendmail/handler})
(defstate tasks-worker (defstate worker
:start (as-> (impl/worker-verticle {:tasks tasks}) $$ :start (impl/start-worker! {:tasks tasks})
(vc/deploy! system $$ {:instances 1}) :stop (impl/stop! worker))
(deref $$)))
;; (defstate scheduler
;; :start (impl/start-scheduler! tasks)
;; :stop (impl/stop! tasks-worker))
;; :start (as-> (impl/worker-verticle {:tasks tasks}) $$
;; (vc/deploy! system $$ {:instances 1})
;; (deref $$)))
;; (def ^:private schedule ;; (def ^:private schedule
;; [{:id "every 1 hour" ;; [{:id "every 1 hour"

View file

@ -12,13 +12,11 @@
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[promesa.core :as p]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]))
[vertx.util :as vu]))
(s/def ::type keyword?) (s/def ::type keyword?)
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
@ -40,42 +38,42 @@
(defmethod handle-deletion :image (defmethod handle-deletion :image
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from image where id=$1 and deleted_at is not null"] (let [sql "delete from image where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :image-collection (defmethod handle-deletion :image-collection
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from image_collection (let [sql "delete from image_collection
where id=$1 and deleted_at is not null"] where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :icon (defmethod handle-deletion :icon
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from icon where id=$1 and deleted_at is not null"] (let [sql "delete from icon where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :icon-collection (defmethod handle-deletion :icon-collection
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from icon_collection (let [sql "delete from icon_collection
where id=$1 and deleted_at is not null"] where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :file (defmethod handle-deletion :file
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from file where id=$1 and deleted_at is not null"] (let [sql "delete from file where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :file-image (defmethod handle-deletion :file-image
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from file_image where id=$1 and deleted_at is not null"] (let [sql "delete from file_image where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :page (defmethod handle-deletion :page
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from page where id=$1 and deleted_at is not null"] (let [sql "delete from page where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :page-version (defmethod handle-deletion :page-version
[conn {:keys [id] :as props}] [conn {:keys [id] :as props}]
(let [sql "delete from page_version where id=$1 and deleted_at is not null"] (let [sql "delete from page_version where id=? and deleted_at is not null"]
(db/query-one conn [sql id]))) (db/exec-one! conn [sql id])))

View file

@ -12,15 +12,12 @@
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[promesa.core :as p]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]))
[vertx.util :as vu]))
(declare select-profile)
(declare delete-profile-data) (declare delete-profile-data)
(declare delete-teams) (declare delete-teams)
(declare delete-files) (declare delete-files)
@ -34,38 +31,32 @@
[{:keys [props] :as task}] [{:keys [props] :as task}]
(us/verify ::props props) (us/verify ::props props)
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (select-profile conn (:profile-id props)) (let [id (:profile-id props)
(p/then (fn [profile] profile (db/get-by-id conn :profile id {:for-update true})]
(if (or (:is-demo profile) (if (or (:is-demo profile)
(not (nil? (:deleted-at profile)))) (not (nil? (:deleted-at profile))))
(delete-profile-data conn (:id profile)) (delete-profile-data conn (:id profile))
(log/warn "Profile " (:id profile) (log/warn "Profile " (:id profile)
"does not match constraints for deletion"))))))) "does not match constraints for deletion")))))
(defn- delete-profile-data (defn- delete-profile-data
[conn profile-id] [conn profile-id]
(log/info "Proceding to delete all data related to profile" profile-id) (log/info "Proceding to delete all data related to profile" profile-id)
(p/do!
(delete-teams conn profile-id) (delete-teams conn profile-id)
(delete-files conn profile-id) (delete-files conn profile-id)
(delete-profile conn profile-id))) (delete-profile conn profile-id))
(def ^:private sql:select-profile (def ^:private sql:select-profile
"select id, is_demo, deleted_at "select id, is_demo, deleted_at
from profile from profile
where id=$1 for update") where id=? for update")
(defn- select-profile
[conn profile-id]
(db/query-one conn [sql:select-profile profile-id]))
(def ^:private sql:remove-owned-teams (def ^:private sql:remove-owned-teams
"with teams as ( "with teams as (
select distinct select distinct
tpr.team_id as id tpr.team_id as id
from team_profile_rel as tpr from team_profile_rel as tpr
where tpr.profile_id = $1 where tpr.profile_id = ?
and tpr.is_owner is true and tpr.is_owner is true
), to_delete_teams as ( ), to_delete_teams as (
select tpr.team_id as id select tpr.team_id as id
@ -80,8 +71,7 @@
(defn- delete-teams (defn- delete-teams
[conn profile-id] [conn profile-id]
(-> (db/query-one conn [sql:remove-owned-teams profile-id]) (db/exec-one! conn [sql:remove-owned-teams profile-id]))
(p/then' (constantly nil))))
(def ^:private sql:remove-owned-files (def ^:private sql:remove-owned-files
"with files_to_delete as ( "with files_to_delete as (
@ -89,7 +79,7 @@
fpr.file_id as id fpr.file_id as id
from file_profile_rel as fpr from file_profile_rel as fpr
inner join file as f on (fpr.file_id = f.id) inner join file as f on (fpr.file_id = f.id)
where fpr.profile_id = $1 where fpr.profile_id = ?
and fpr.is_owner is true and fpr.is_owner is true
and f.project_id is null and f.project_id is null
) )
@ -99,12 +89,8 @@
(defn- delete-files (defn- delete-files
[conn profile-id] [conn profile-id]
(-> (db/query-one conn [sql:remove-owned-files profile-id]) (db/exec-one! conn [sql:remove-owned-files profile-id]))
(p/then' (constantly nil))))
(defn delete-profile (defn delete-profile
[conn profile-id] [conn profile-id]
(let [sql "delete from profile where id=$1"] (db/delete! conn :profile {:id profile-id}))
(-> (db/query conn [sql profile-id])
(p/then' (constantly profile-id)))))

View file

@ -10,24 +10,24 @@
(ns uxbox.tasks.impl (ns uxbox.tasks.impl
"Async tasks implementation." "Async tasks implementation."
(:require (:require
[clojure.core.async :as a]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[mount.core :as mount :refer [defstate]]
[promesa.core :as p]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.core :refer [system]]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.util.time :as tm] [uxbox.util.time :as dt])
[vertx.core :as vc]
[vertx.util :as vu]
[vertx.timers :as vt])
(:import (:import
java.time.Duration java.time.Duration
java.time.Instant java.time.Instant
java.util.Date)) java.util.Date))
(defrecord Worker [stop]
java.lang.AutoCloseable
(close [_] (a/close! stop)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Tasks ;; Tasks
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -40,42 +40,42 @@
(def ^:private sql:mark-as-retry (def ^:private sql:mark-as-retry
"update task "update task
set scheduled_at = clock_timestamp() + '5 seconds'::interval, set scheduled_at = clock_timestamp() + '5 seconds'::interval,
error = $1, error = ?,
status = 'retry', status = 'retry',
retry_num = retry_num + 1 retry_num = retry_num + 1
where id = $2;") where id = ?")
(defn- reschedule (defn- reschedule
[conn task error] [conn task error]
(let [explain (ex-message error) (let [explain (ex-message error)
sqlv [sql:mark-as-retry explain (:id task)]] sqlv [sql:mark-as-retry explain (:id task)]]
(-> (db/query-one conn sqlv) (db/exec-one! conn sqlv)
(p/then' (constantly nil))))) nil))
(def ^:private sql:mark-as-failed (def ^:private sql:mark-as-failed
"update task "update task
set scheduled_at = clock_timestamp() + '5 seconds'::interval, set scheduled_at = clock_timestamp() + '5 seconds'::interval,
error = $1, error = ?,
status = 'failed' status = 'failed'
where id = $2;") where id = ?;")
(defn- mark-as-failed (defn- mark-as-failed
[conn task error] [conn task error]
(let [explain (ex-message error) (let [explain (ex-message error)
sqlv [sql:mark-as-failed explain (:id task)]] sqlv [sql:mark-as-failed explain (:id task)]]
(-> (db/query-one conn sqlv) (db/exec-one! conn sqlv)
(p/then' (constantly nil))))) nil))
(def ^:private sql:mark-as-completed (def ^:private sql:mark-as-completed
"update task "update task
set completed_at = clock_timestamp(), set completed_at = clock_timestamp(),
status = 'completed' status = 'completed'
where id = $1") where id = ?")
(defn- mark-as-completed (defn- mark-as-completed
[conn task] [conn task]
(-> (db/query-one conn [sql:mark-as-completed (:id task)]) (db/exec-one! conn [sql:mark-as-completed (:id task)])
(p/then' (constantly nil)))) nil)
(defn- handle-task (defn- handle-task
[tasks {:keys [name] :as item}] [tasks {:keys [name] :as item}]
@ -89,8 +89,8 @@
(def ^:private sql:select-next-task (def ^:private sql:select-next-task
"select * from task as t "select * from task as t
where t.scheduled_at <= now() where t.scheduled_at <= now()
and t.queue = $1 and t.queue = ?
and (t.status = 'new' or (t.status = 'retry' and t.retry_num <= $2)) and (t.status = 'new' or (t.status = 'retry' and t.retry_num <= ?))
order by t.scheduled_at order by t.scheduled_at
limit 1 limit 1
for update skip locked") for update skip locked")
@ -108,124 +108,130 @@
(with-out-str (with-out-str
(.printStackTrace ^Throwable err (java.io.PrintWriter. *out*))))) (.printStackTrace ^Throwable err (java.io.PrintWriter. *out*)))))
(defn- event-loop (defn- event-loop-fn
[{:keys [tasks] :as options}] [{:keys [tasks] :as options}]
(let [queue (:queue options "default") (let [queue (:queue options "default")
max-retries (:max-retries options 3)] max-retries (:max-retries options 3)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (db/query-one conn [sql:select-next-task queue max-retries]) (let [item (-> (db/exec-one! conn [sql:select-next-task queue max-retries])
(p/then decode-task-row) (decode-task-row))]
(p/then (fn [item]
(when item (when item
(log/info "Execute task" (:name item)) (log/info "Execute task" (:name item))
(-> (p/do! (handle-task tasks item)) (try
(p/handle (fn [v e] (handle-task tasks item)
(if e (mark-as-completed conn item)
(do ::handled
(catch Throwable e
(log-task-error item e) (log-task-error item e)
(if (>= (:retry-num item) max-retries) (if (>= (:retry-num item) max-retries)
(mark-as-failed conn item e) (mark-as-failed conn item e)
(reschedule conn item e))) (reschedule conn item e)))))))))
(mark-as-completed conn item))))
(p/then' (constantly ::handled))))))))))
(defn- event-loop-handler (defn- start-worker-eventloop!
[options] [options]
(let [counter (::counter options 1) (let [stop (::stop options)
mbs (:max-batch-size options 10)] mbs (:max-batch-size options 10)]
(-> (event-loop options) (a/go-loop []
(p/then (fn [result] (let [timeout (a/timeout 5000)
(when (and (= result ::handled) [val port] (a/alts! [stop timeout])]
(> mbs counter)) (when (= port timeout)
(event-loop-handler (assoc options ::counter (inc counter))))))))) (a/<! (a/thread
;; Tasks batching in one event loop execution.
(def ^:private sql:insert-new-task (loop [cnt 1
"insert into task (name, props, queue, scheduled_at) res (event-loop-fn options)]
values ($1, $2, $3, clock_timestamp()+cast($4::text as interval)) (when (and (= res ::handled)
returning id") (> mbs cnt))
(recur (inc 1)
(event-loop-fn options))))))
(recur))))))
(defn- duration->pginterval (defn- duration->pginterval
[^Duration d] [^Duration d]
(->> (/ (.toMillis d) 1000.0) (->> (/ (.toMillis d) 1000.0)
(format "%s seconds"))) (format "%s seconds")))
(defn- on-worker-start (defn start-worker!
[ctx {:keys [tasks] :as options}] [options]
(vt/schedule! ctx (assoc options (let [stop (a/chan)]
::vt/fn #'event-loop-handler (a/go
::vt/delay 5000 (a/<! (start-worker-eventloop! (assoc options ::stop stop)))
::vt/repeat true))) (log/info "STOPING"))
(->Worker stop)))
(defn stop!
[worker]
(.close ^java.lang.AutoCloseable worker))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Scheduled Tasks ;; Scheduled Tasks
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:privatr sql:upsert-scheduled-task ;; (def ^:privatr sql:upsert-scheduled-task
"insert into scheduled_task (id, cron_expr) ;; "insert into scheduled_task (id, cron_expr)
values ($1, $2) ;; values ($1, $2)
on conflict (id) ;; on conflict (id)
do update set cron_expr=$2") ;; do update set cron_expr=$2")
(defn- synchronize-schedule-item ;; (defn- synchronize-schedule-item
[conn {:keys [id cron]}] ;; [conn {:keys [id cron]}]
(-> (db/query-one conn [sql:upsert-scheduled-task id (str cron)]) ;; (-> (db/query-one conn [sql:upsert-scheduled-task id (str cron)])
(p/then' (constantly nil)))) ;; (p/then' (constantly nil))))
(defn- synchronize-schedule ;; (defn- synchronize-schedule
[schedule] ;; [schedule]
(db/with-atomic [conn db/pool] ;; (db/with-atomic [conn db/pool]
(p/run! (partial synchronize-schedule-item conn) schedule))) ;; (p/run! (partial synchronize-schedule-item conn) schedule)))
(def ^:private sql:lock-scheduled-task ;; (def ^:private sql:lock-scheduled-task
"select id from scheduled_task where id=$1 for update skip locked") ;; "select id from scheduled_task where id=$1 for update skip locked")
(declare schedule-task) ;; (declare schedule-task)
(defn- log-scheduled-task-error ;; (defn- log-scheduled-task-error
[item err] ;; [item err]
(log/error "Unhandled exception on scheduled task '" (:id item) "' \n" ;; (log/error "Unhandled exception on scheduled task '" (:id item) "' \n"
(with-out-str ;; (with-out-str
(.printStackTrace ^Throwable err (java.io.PrintWriter. *out*))))) ;; (.printStackTrace ^Throwable err (java.io.PrintWriter. *out*)))))
(defn- execute-scheduled-task ;; (defn- execute-scheduled-task
[{:keys [id cron] :as stask}] ;; [{:keys [id cron] :as stask}]
(db/with-atomic [conn db/pool] ;; (db/with-atomic [conn db/pool]
;; First we try to lock the task in the database, if locking us ;; ;; First we try to lock the task in the database, if locking us
;; successful, then we execute the scheduled task; if locking is ;; ;; successful, then we execute the scheduled task; if locking is
;; not possible (because other instance is already locked id) we ;; ;; not possible (because other instance is already locked id) we
;; just skip it and schedule to be executed in the next slot. ;; ;; just skip it and schedule to be executed in the next slot.
(-> (db/query-one conn [sql:lock-scheduled-task id]) ;; (-> (db/query-one conn [sql:lock-scheduled-task id])
(p/then (fn [result] ;; (p/then (fn [result]
(when result ;; (when result
(-> (p/do! ((:fn stask) stask)) ;; (-> (p/do! ((:fn stask) stask))
(p/catch (fn [e] ;; (p/catch (fn [e]
(log-scheduled-task-error stask e) ;; (log-scheduled-task-error stask e)
nil)))))) ;; nil))))))
(p/finally (fn [v e] ;; (p/finally (fn [v e]
(-> (vu/current-context) ;; (-> (vu/current-context)
(schedule-task stask))))))) ;; (schedule-task stask)))))))
(defn ms-until-valid ;; (defn ms-until-valid
[cron] ;; [cron]
(s/assert tm/cron? cron) ;; (s/assert dt/cron? cron)
(let [^Instant now (tm/now) ;; (let [^Instant now (dt/now)
^Instant next (tm/next-valid-instant-from cron now) ;; ^Instant next (dt/next-valid-instant-from cron now)
^Duration duration (Duration/between now next)] ;; ^Duration duration (Duration/between now next)]
(.toMillis duration))) ;; (.toMillis duration)))
(defn- schedule-task ;; (defn- schedule-task
[ctx {:keys [cron] :as stask}] ;; [ctx {:keys [cron] :as stask}]
(let [ms (ms-until-valid cron)] ;; (let [ms (ms-until-valid cron)]
(vt/schedule! ctx (assoc stask ;; (vt/schedule! ctx (assoc stask
:ctx ctx ;; :ctx ctx
::vt/once true ;; ::vt/once true
::vt/delay ms ;; ::vt/delay ms
::vt/fn execute-scheduled-task)))) ;; ::vt/fn execute-scheduled-task))))
(defn- on-scheduler-start ;; (defn- on-scheduler-start
[ctx {:keys [schedule] :as options}] ;; [ctx {:keys [schedule] :as options}]
(-> (synchronize-schedule schedule) ;; (-> (synchronize-schedule schedule)
(p/then' (fn [_] ;; (p/then' (fn [_]
(run! #(schedule-task ctx %) schedule))))) ;; (run! #(schedule-task ctx %) schedule)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Public API ;; Public API
@ -233,64 +239,71 @@
;; --- Worker Verticle ;; --- Worker Verticle
(s/def ::callable (s/or :fn fn? :var var?)) ;; (s/def ::callable (s/or :fn fn? :var var?))
(s/def ::max-batch-size ::us/integer) ;; (s/def ::max-batch-size ::us/integer)
(s/def ::max-retries ::us/integer) ;; (s/def ::max-retries ::us/integer)
(s/def ::tasks (s/map-of string? ::callable)) ;; (s/def ::tasks (s/map-of string? ::callable))
(s/def ::worker-verticle-options ;; (s/def ::worker-verticle-options
(s/keys :req-un [::tasks] ;; (s/keys :req-un [::tasks]
:opt-un [::queue ::max-batch-size])) ;; :opt-un [::queue ::max-batch-size]))
(defn worker-verticle ;; (defn worker-verticle
[options] ;; [options]
(s/assert ::worker-verticle-options options) ;; (s/assert ::worker-verticle-options options)
(let [on-start #(on-worker-start % options)] ;; (let [on-start #(on-worker-start % options)]
(vc/verticle {:on-start on-start}))) ;; (vc/verticle {:on-start on-start})))
;; --- Scheduler Verticle ;; --- Scheduler Verticle
(s/def ::id string?) ;; (s/def ::id string?)
(s/def ::cron tm/cron?) ;; (s/def ::cron dt/cron?)
(s/def ::fn ::callable) ;; (s/def ::fn ::callable)
(s/def ::props (s/nilable map?)) ;; (s/def ::props (s/nilable map?))
(s/def ::scheduled-task ;; (s/def ::scheduled-task
(s/keys :req-un [::id ::cron ::fn] ;; (s/keys :req-un [::id ::cron ::fn]
:opt-un [::props])) ;; :opt-un [::props]))
(s/def ::schedule (s/coll-of ::scheduled-task)) ;; (s/def ::schedule (s/coll-of ::scheduled-task))
(s/def ::scheduler-verticle-options ;; (s/def ::scheduler-verticle-options
(s/keys :opt-un [::schedule])) ;; (s/keys :opt-un [::schedule]))
(defn scheduler-verticle ;; (defn scheduler-verticle
[options] ;; [options]
(s/assert ::scheduler-verticle-options options) ;; (s/assert ::scheduler-verticle-options options)
(let [on-start #(on-scheduler-start % options)] ;; (let [on-start #(on-scheduler-start % options)]
(vc/verticle {:on-start on-start}))) ;; (vc/verticle {:on-start on-start})))
;; --- Schedule API ;; --- Schedule API
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::delay (s/def ::delay
(s/or :int ::us/integer (s/or :int ::us/integer
:duration tm/duration?)) :duration dt/duration?))
(s/def ::queue ::us/string) (s/def ::queue ::us/string)
(s/def ::task-options (s/def ::task-options
(s/keys :req-un [::name] (s/keys :req-un [::name]
:opt-un [::delay ::props ::queue])) :opt-un [::delay ::props ::queue]))
(def ^:private sql:insert-new-task
"insert into task (id, name, props, queue, scheduled_at)
values (?, ?, ?, ?, clock_timestamp()+cast(?::text as interval))
returning id")
(defn schedule! (defn schedule!
[conn {:keys [name delay props queue key] [conn {:keys [name delay props queue key]
:or {delay 0 props {} queue "default"} :or {delay 0 props {} queue "default"}
:as options}] :as options}]
(us/verify ::task-options options) (us/verify ::task-options options)
(let [duration (tm/duration delay) (let [duration (dt/duration delay)
pginterval (duration->pginterval duration) pginterval (duration->pginterval duration)
props (blob/encode props)] props (blob/encode props)
id (uuid/next)]
(log/info "Schedule task" name (log/info "Schedule task" name
;; "with props" (pr-str props) ;; "with props" (pr-str props)
"to be executed in" (str duration)) "to be executed in" (str duration))
(-> (db/query-one conn [sql:insert-new-task name props queue pginterval]) (db/exec-one! conn [sql:insert-new-task
(p/then' (fn [task] (:id task)))))) id name props queue pginterval])
id))

View file

@ -15,8 +15,7 @@
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.media :as media] [uxbox.media :as media]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]))
[vertx.util :as vu]))
(s/def ::path ::us/not-empty-string) (s/def ::path ::us/not-empty-string)
(s/def ::props (s/def ::props
@ -25,8 +24,7 @@
(defn handler (defn handler
[{:keys [props] :as task}] [{:keys [props] :as task}]
(us/verify ::props props) (us/verify ::props props)
(vu/blocking
(when (ust/exists? media/media-storage (:path props)) (when (ust/exists? media/media-storage (:path props))
(ust/delete! media/media-storage (:path props)) (ust/delete! media/media-storage (:path props))
(log/debug "Media " (:path props) " removed.")))) (log/debug "Media " (:path props) " removed.")))

View file

@ -12,12 +12,10 @@
[clojure.data.json :as json] [clojure.data.json :as json]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[postal.core :as postal] [postal.core :as postal]
[promesa.core :as p]
[uxbox.common.data :as d] [uxbox.common.data :as d]
[uxbox.common.exceptions :as ex] [uxbox.common.exceptions :as ex]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.util.http :as http] [uxbox.util.http :as http]))
[vertx.util :as vu]))
(defmulti sendmail (fn [config email] (:sendmail-backend config))) (defmulti sendmail (fn [config email] (:sendmail-backend config)))
@ -49,21 +47,17 @@
headers {"Authorization" (str "Bearer " apikey) headers {"Authorization" (str "Bearer " apikey)
"Content-Type" "application/json"} "Content-Type" "application/json"}
body (json/write-str params)] body (json/write-str params)]
(-> (http/send! {:method :post
(try
(let [response (http/send! {:method :post
:headers headers :headers headers
:uri "https://api.sendgrid.com/v3/mail/send" :uri "https://api.sendgrid.com/v3/mail/send"
:body body}) :body body})]
(p/handle (when-not (= 202 (:status response))
(fn [response error] (log/error "Unexpected status from sendgrid:" (pr-str response))))
(cond (catch Throwable error
error (log/error "Error on sending email to sendgrid:" (pr-str error))))))
(log/error "Error on sending email to sendgrid:" (pr-str error))
(= 202 (:status response))
nil
:else
(log/error "Unexpected status from sendgrid:" (pr-str response))))))))
(defn- get-smtp-config (defn- get-smtp-config
[config] [config]
@ -87,14 +81,13 @@
(defmethod sendmail "smtp" (defmethod sendmail "smtp"
[config email] [config email]
(vu/blocking
(let [config (get-smtp-config config) (let [config (get-smtp-config config)
email (email->postal email) email (email->postal email)
result (postal/send-message config email)] result (postal/send-message config email)]
(when (not= (:error result) :SUCCESS) (when (not= (:error result) :SUCCESS)
(ex/raise :type :sendmail-error (ex/raise :type :sendmail-error
:code :email-not-sent :code :email-not-sent
:context result))))) :context result))))
(defn handler (defn handler
{:uxbox.tasks/name "sendmail"} {:uxbox.tasks/name "sendmail"}

View file

@ -12,7 +12,6 @@
page data, page options and txlog payload storage." page data, page options and txlog payload storage."
(:require [uxbox.util.transit :as t]) (:require [uxbox.util.transit :as t])
(:import (:import
io.vertx.core.buffer.Buffer
java.io.ByteArrayInputStream java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream java.io.ByteArrayOutputStream
java.io.DataInputStream java.io.DataInputStream
@ -28,9 +27,6 @@
(Class/forName "[B") (Class/forName "[B")
(->bytes [data] data) (->bytes [data] data)
Buffer
(->bytes [data] (.getBytes ^Buffer data))
String String
(->bytes [data] (.getBytes ^String data "UTF-8"))) (->bytes [data] (.getBytes ^String data "UTF-8")))
@ -49,8 +45,7 @@
(.writeShort dos (short 1)) ;; version number (.writeShort dos (short 1)) ;; version number
(.writeInt dos (int data-len)) (.writeInt dos (int data-len))
(.write dos ^bytes cdata (int 0) clen) (.write dos ^bytes cdata (int 0) clen)
(-> (.toByteArray baos) (.toByteArray baos))))
(t/bytes->buffer)))))
(declare decode-v1) (declare decode-v1)

View file

@ -9,7 +9,6 @@
(:refer-clojure :exclude [defmethod]) (:refer-clojure :exclude [defmethod])
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p]
[expound.alpha :as expound] [expound.alpha :as expound]
[uxbox.common.exceptions :as ex]) [uxbox.common.exceptions :as ex])
(:import (:import
@ -127,14 +126,10 @@
(with-meta (with-meta
(fn [params] (fn [params]
(try (try
(-> (handler params) (handler params)
(p/catch' (fn [error] (catch Throwable error
(ex/raise :type :service-error (ex/raise :type :service-error
:name (:spec mdata) :name (:spec mdata)
:cause error)))) :cause error))))
(catch Throwable error
(p/rejected (ex/error :type :service-error
:name (:spec mdata)
:cause error)))))
(assoc mdata ::wrap-error true)))) (assoc mdata ::wrap-error true))))

View file

@ -16,4 +16,4 @@
(defn send! (defn send!
[req] [req]
(http/send-async req {:client @default-client :as :string})) (http/send req {:client @default-client :as :string}))

View file

@ -10,8 +10,8 @@
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[promesa.core :as p] [next.jdbc :as jdbc]))
[uxbox.util.pgsql :as pg]))
(s/def ::name string?) (s/def ::name string?)
(s/def ::step (s/keys :req-un [::name ::desc ::fn])) (s/def ::step (s/keys :req-un [::name ::desc ::fn]))
@ -24,75 +24,57 @@
(defn- registered? (defn- registered?
"Check if concrete migration is already registred." "Check if concrete migration is already registred."
[pool modname stepname] [pool modname stepname]
(let [sql "select * from migrations where module=$1 and step=$2"] (let [sql "select * from migrations where module=? and step=?"
(-> (pg/query pool [sql modname stepname]) rows (jdbc/execute! pool [sql modname stepname])]
(p/then' (fn [rows] (pos? (count rows))))
(pos? (count rows)))))))
(defn- register! (defn- register!
"Register a concrete migration into local migrations database." "Register a concrete migration into local migrations database."
[pool modname stepname] [pool modname stepname]
(let [sql "insert into migrations (module, step) values ($1, $2)"] (let [sql "insert into migrations (module, step) values (?, ?)"]
(-> (pg/query pool [sql modname stepname]) (jdbc/execute! pool [sql modname stepname])
(p/then' (constantly nil))))) nil))
(defn- impl-migrate-single
[pool modname {:keys [name] :as migration}]
(letfn [(execute []
(register! pool modname name)
((:fn migration) pool))]
(when-not (registered? pool modname (:name migration))
(log/info (str/format "applying migration %s/%s" modname name))
(register! pool modname name)
((:fn migration) pool))))
(defn- setup! (defn- impl-migrate
[conn migrations {:keys [fake] :or {fake false}}]
(s/assert ::migrations migrations)
(let [mname (:name migrations)
steps (:steps migrations)]
(jdbc/with-transaction [conn conn]
(run! #(impl-migrate-single conn mname %) steps))))
(defprotocol IMigrationContext
(-migrate [_ migration options]))
;; --- Public Api
(defn setup!
"Initialize the database if it is not initialized." "Initialize the database if it is not initialized."
[pool] [conn]
(let [sql (str "create table if not exists migrations (" (let [sql (str "create table if not exists migrations ("
" module text," " module text,"
" step text," " step text,"
" created_at timestamp DEFAULT current_timestamp," " created_at timestamp DEFAULT current_timestamp,"
" unique(module, step)" " unique(module, step)"
");")] ");")]
(-> (pg/query pool sql) (jdbc/execute! conn [sql])
(p/then' (constantly nil))))) nil))
(defn- impl-migrate-single (defn migrate!
[pool modname {:keys [name] :as migration}]
(letfn [(execute []
(p/do! (register! pool modname name)
((:fn migration) pool)))]
(-> (registered? pool modname (:name migration))
(p/then (fn [registered?]
(when-not registered?
(log/info (str/format "applying migration %s/%s" modname name))
(execute)))))))
(defn- impl-migrate
[pool migrations {:keys [fake] :or {fake false}}]
(s/assert ::migrations migrations)
(let [mname (:name migrations)
steps (:steps migrations)]
;; (println (str/format "Applying migrations for `%s`:" mname))
(pg/with-atomic [conn pool]
(p/run! #(impl-migrate-single conn mname %) steps))))
(defprotocol IMigrationContext
(-migrate [_ migration options]))
;; --- Public Api
(defn context
"Create new instance of migration context."
([pool] (context pool nil))
([pool opts]
@(setup! pool)
(reify
java.lang.AutoCloseable
(close [_] #_(.close pool))
IMigrationContext
(-migrate [_ migration options]
(impl-migrate pool migration options)))))
(defn migrate
"Main entry point for apply a migration." "Main entry point for apply a migration."
([ctx migrations] ([conn migrations]
(migrate ctx migrations nil)) (migrate! conn migrations nil))
([ctx migrations options] ([conn migrations options]
(-migrate ctx migrations options))) (impl-migrate conn migrations options)))
(defn resource (defn resource
"Helper for setup migration functions "Helper for setup migration functions
@ -101,5 +83,5 @@
[path] [path]
(fn [pool] (fn [pool]
(let [sql (slurp (io/resource path))] (let [sql (slurp (io/resource path))]
(-> (pg/query pool sql) (jdbc/execute! pool [sql])
(p/then' (constantly true)))))) true)))

View file

@ -1,162 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.pgsql
"Asynchronous posgresql client."
(:require
[promesa.core :as p])
(:import
clojure.lang.IDeref
java.util.function.Supplier
java.lang.ThreadLocal
io.vertx.core.Vertx
io.vertx.core.Handler
io.vertx.core.AsyncResult
io.vertx.core.buffer.Buffer
io.vertx.pgclient.PgPool
io.vertx.pgclient.PgConnection
io.vertx.sqlclient.impl.ArrayTuple
io.vertx.sqlclient.SqlClient
io.vertx.sqlclient.RowSet
io.vertx.sqlclient.Row
io.vertx.sqlclient.Tuple
io.vertx.sqlclient.Transaction
io.vertx.sqlclient.PoolOptions))
(declare impl-execute)
(declare impl-query)
(declare impl-handler)
(declare impl-transact)
(declare seqable->tuple)
;; --- Public Api
(defn vertx?
[v]
(instance? Vertx v))
(defn pool?
[v]
(instance? PgPool v))
(defn bytes->buffer
[data]
(Buffer/buffer ^bytes data))
(defn pool
([uri] (pool uri {}))
([uri {:keys [system max-size] :or {max-size 5}}]
(let [^PoolOptions poptions (PoolOptions.)]
(when max-size (.setMaxSize poptions max-size))
(if (vertx? system)
(PgPool/pool ^Vertx system ^String uri poptions)
(PgPool/pool ^String uri poptions)))))
(defn tl-pool
"Thread local based connection pool."
([uri] (tl-pool uri {}))
([uri options]
(let [state (ThreadLocal/withInitial (reify Supplier
(get [_]
(pool uri options))))]
(reify IDeref
(deref [_]
(.get state))))))
(defn query
([conn sqlv] (query conn sqlv {}))
([conn sqlv opts]
(cond
(vector? sqlv)
(impl-query conn (first sqlv) (rest sqlv) opts)
(string? sqlv)
(impl-query conn sqlv nil opts)
:else
(throw (ex-info "Invalid arguments" {:sqlv sqlv})))))
(defn query-one
[& args]
(p/map first (apply query args)))
(defn row->map
[^Row row]
(reduce (fn [acc index]
(let [cname (.getColumnName row index)]
(if-some [value (.getValue row ^int index)]
(assoc acc cname value)
acc)))
{}
(range (.size row))))
(defmacro with-atomic
[[bsym psym] & body]
`(impl-transact ~psym (fn [c#] (let [~bsym c#] ~@body))))
;; --- Implementation
(defn- seqable->tuple
[v]
(let [res (ArrayTuple. (count v))]
(run! #(.addValue res %) v)
res))
(defn- impl-handler
[resolve reject]
(reify Handler
(handle [_ ar]
(if (.failed ^AsyncResult ar)
(reject (.cause ^AsyncResult ar))
(resolve (.result ^AsyncResult ar))))))
(defn- impl-execute
[^SqlClient conn ^String sql params]
(if (seq params)
(p/create #(.preparedQuery conn sql
^Tuple (seqable->tuple params)
^Handler (impl-handler %1 %2)))
(p/create #(.query conn sql
^Handler (impl-handler %1 %2)))))
(defn- impl-query
[^SqlClient conn ^String sql params {:keys [xfm] :as opts}]
(let [conn (if (instance? IDeref conn) @conn conn)]
(-> (impl-execute conn sql params)
(p/catch (fn [err]
(p/rejected err)))
(p/then' (fn [rows]
(if xfm
(into [] xfm rows)
(into [] (map vec) rows)))))))
(defn impl-transact
[pool f]
(let [pool (if (instance? IDeref pool) @pool pool)]
(letfn [(commit [^Transaction tx]
(p/create #(.commit tx (impl-handler %1 %2))))
(rollback [^Transaction tx]
(p/create #(.rollback tx (impl-handler %1 %2))))
(on-connect [^PgConnection conn]
(let [tx (.begin conn)
df (p/deferred)]
(-> (f conn)
(p/finally (fn [v e]
(if e
(-> (rollback tx)
(p/finally (fn [& args]
(.close conn)
(p/reject! df e))))
(-> (commit tx)
(p/finally (fn [_ e']
(.close conn)
(if e'
(p/reject! df e')
(p/resolve! df v)))))))))
df))]
(-> (p/create #(.getConnection ^PgPool pool (impl-handler %1 %2)))
(p/bind on-connect)))))

View file

@ -40,15 +40,14 @@
(defn connect (defn connect
[client] [client]
(let [^RedisURI uri (:uri client) (let [^RedisURI uri (:uri client)
^RedisClient client (:client client)] ^RedisClient client (:client client)
(-> (.connectAsync client StringCodec/UTF8 uri) ^StatefulRedisConnection conn (.connect client StringCodec/UTF8 uri)]
(p/then' (fn [^StatefulRedisConnection conn] (->Connection (.async conn))))
(->Connection (.async conn)))))))
(defn- impl-subscribe (defn- impl-subscribe
[^String topic ^StatefulRedisPubSubConnection conn] [^String topic xf ^StatefulRedisPubSubConnection conn]
(let [cmd (.async conn) (let [cmd (.sync conn)
output (a/chan 1 (filter string?)) output (a/chan 1 (comp (filter string?) xf))
buffer (a/chan (a/sliding-buffer 64)) buffer (a/chan (a/sliding-buffer 64))
sub (reify RedisPubSubListener sub (reify RedisPubSubListener
(message [it pattern channel message]) (message [it pattern channel message])
@ -74,15 +73,17 @@
(when (.isOpen conn) (when (.isOpen conn)
(.close conn)))))) (.close conn))))))
(-> (.subscribe ^RedisPubSubAsyncCommands cmd (into-array String [topic])) (.subscribe ^RedisPubSubCommands cmd (into-array String [topic]))
(p/then' (constantly output))))) output))
(defn subscribe (defn subscribe
[client topic] ([client topic]
(subscribe client topic (map identity)))
([client topic xf]
(let [^RedisURI uri (:uri client) (let [^RedisURI uri (:uri client)
^RedisClient client (:client client)] ^RedisClient client (:client client)]
(-> (.connectPubSubAsync client StringCodec/UTF8 uri) (->> (.connectPubSub client StringCodec/UTF8 uri)
(p/then (partial impl-subscribe topic))))) (impl-subscribe topic xf)))))
(defn- resolve-to-bool (defn- resolve-to-bool
[v] [v]

View file

@ -11,7 +11,6 @@
[uxbox.util.time :as dt] [uxbox.util.time :as dt]
[uxbox.util.data :as data]) [uxbox.util.data :as data])
(:import (:import
io.vertx.core.buffer.Buffer
java.io.ByteArrayInputStream java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream java.io.ByteArrayOutputStream
java.io.File)) java.io.File))
@ -85,12 +84,3 @@
([^bytes data, ^String encoding] ([^bytes data, ^String encoding]
(String. data encoding))) (String. data encoding)))
(defn bytes->buffer
[^bytes data]
(Buffer/buffer data))
(defn buffer->bytes
[^Buffer data]
(.getBytes data))

View file

@ -1,7 +1,9 @@
(ns uxbox.tests.helpers (ns uxbox.tests.helpers
(:require (:require
[clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[datoteka.core :as fs]
[cuerdas.core :as str] [cuerdas.core :as str]
[mount.core :as mount] [mount.core :as mount]
[environ.core :refer [env]] [environ.core :refer [env]]
@ -21,27 +23,34 @@
[uxbox.util.blob :as blob] [uxbox.util.blob :as blob]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]
[uxbox.config :as cfg] [uxbox.config :as cfg])
[vertx.util :as vu])) (:import org.postgresql.ds.PGSimpleDataSource))
(def ^:dynamic *context* nil) (defn testing-datasource
[]
(doto (PGSimpleDataSource.)
(.setServerName "postgres")
(.setDatabaseName "uxbox_test")
(.setUser "uxbox")
(.setPassword "uxbox")))
(defn state-init (defn state-init
[next] [next]
(let [config (cfg/read-test-config env)] (let [config (cfg/read-test-config env)]
(try
;; (Class/forName "org.postgresql.Driver")
(let [pool (testing-datasource)]
(-> (mount/only #{#'uxbox.config/config (-> (mount/only #{#'uxbox.config/config
#'uxbox.core/system
#'uxbox.db/pool #'uxbox.db/pool
#'uxbox.services.init/query-services #'uxbox.services.init/query-services
#'uxbox.services.init/mutation-services #'uxbox.services.init/mutation-services
#'uxbox.migrations/migrations #'uxbox.migrations/migrations
#'uxbox.media/assets-storage #'uxbox.media/assets-storage
#'uxbox.media/media-storage}) #'uxbox.media/media-storage})
(mount/swap {#'uxbox.config/config config}) (mount/swap {#'uxbox.config/config config
(mount/start)) #'uxbox.db/pool pool})
(try (mount/start)))
(binding [*context* (vu/get-or-create-context uxbox.core/system)] (next)
(next))
(finally (finally
(mount/stop))))) (mount/stop)))))
@ -51,14 +60,12 @@
" FROM information_schema.tables " " FROM information_schema.tables "
" WHERE table_schema = 'public' " " WHERE table_schema = 'public' "
" AND table_name != 'migrations';")] " AND table_name != 'migrations';")]
(db/with-atomic [conn db/pool]
@(db/with-atomic [conn db/pool] (let [result (->> (db/exec! conn [sql])
(-> (db/query conn sql) (map :table-name))]
(p/then #(map :table-name %)) (db/exec! conn [(str "TRUNCATE "
(p/then (fn [result]
(db/query-one conn (str "TRUNCATE "
(apply str (interpose ", " result)) (apply str (interpose ", " result))
" CASCADE;"))))))) " CASCADE;")]))))
(try (try
(next) (next)
(finally (finally
@ -142,15 +149,8 @@
(defmacro try-on! (defmacro try-on!
[expr] [expr]
`(try `(try
(let [d# (p/deferred)] {:error nil
(->> #(p/finally (p/do! ~expr) :result ~expr}
(fn [v# e#]
(if e#
(p/reject! d# e#)
(p/resolve! d# v#))))
(vu/run-on-context! *context*))
(array-map :error nil
:result (deref d#)))
(catch Exception e# (catch Exception e#
{:error (handle-error e#) {:error (handle-error e#)
:result nil}))) :result nil})))
@ -211,3 +211,11 @@
[e code] [e code]
(let [data (ex-data e)] (let [data (ex-data e)]
(= code (:code data)))) (= code (:code data))))
(defn tempfile
[source]
(let [rsc (io/resource source)
tmp (fs/create-tempfile)]
(io/copy (io/file rsc)
(io/file tmp))
tmp))

View file

@ -20,10 +20,10 @@
(let [result (emails/render emails/register {:to "example@uxbox.io" :name "foo"})] (let [result (emails/render emails/register {:to "example@uxbox.io" :name "foo"})]
(t/is (map? result)) (t/is (map? result))
(t/is (contains? result :subject)) (t/is (contains? result :subject))
(t/is (contains? result :body)) (t/is (contains? result :content))
(t/is (contains? result :to)) (t/is (contains? result :to))
(t/is (contains? result :reply-to)) (t/is (contains? result :reply-to))
(t/is (vector? (:body result))))) (t/is (vector? (:content result)))))
;; (t/deftest email-sending-and-sendmail-job ;; (t/deftest email-sending-and-sendmail-job
;; (let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})] ;; (let [res @(emails/send! emails/register {:to "example@uxbox.io" :name "foo"})]

View file

@ -1,31 +1,28 @@
(ns uxbox.tests.test-services-colors (ns uxbox.tests.test-services-colors
(:require (:require
[clojure.test :as t] [clojure.test :as t]
[promesa.core :as p]
[datoteka.core :as fs] [datoteka.core :as fs]
[clojure.java.io :as io] [clojure.java.io :as io]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.core :refer [system]]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]
[uxbox.common.uuid :as uuid] [uxbox.common.uuid :as uuid]
[uxbox.tests.helpers :as th] [uxbox.tests.helpers :as th]))
[vertx.core :as vc]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest color-libraries-crud (t/deftest color-libraries-crud
(let [id (uuid/next) (let [id (uuid/next)
prof @(th/create-profile db/pool 2) prof (th/create-profile db/pool 2)
team (:default-team prof)] team-id (:default-team prof)]
(t/testing "create library" (t/testing "create library"
(let [data {::sm/type :create-color-library (let [data {::sm/type :create-color-library
:name "sample library" :name "sample library"
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team) :team-id team-id
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
@ -34,7 +31,7 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= id (:id result))) (t/is (= id (:id result)))
(t/is (= (:id team) (:team-id result))) (t/is (= team-id (:team-id result)))
(t/is (= (:name data) (:name result)))))) (t/is (= (:name data) (:name result))))))
(t/testing "update library" (t/testing "update library"
@ -44,23 +41,11 @@
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/testing "query libraries"
(let [data {::sq/type :color-libraries
:profile-id (:id prof)
:team-id (:id team)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= 1 (count result))) (t/is (= "renamed" (get-in result [:name]))))))
(t/is (= (:id team) (get-in result [0 :team-id])))
(t/is (= "renamed" (get-in result [0 :name]))))))
(t/testing "delete library" (t/testing "delete library"
(let [data {::sm/type :delete-color-library (let [data {::sm/type :delete-color-library
@ -76,7 +61,7 @@
(t/testing "query libraries after delete" (t/testing "query libraries after delete"
(let [data {::sq/type :color-libraries (let [data {::sq/type :color-libraries
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team)} :team-id team-id}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -86,9 +71,9 @@
)) ))
(t/deftest colors-crud (t/deftest colors-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
coll @(th/create-color-library db/pool (:id team) 1) coll (th/create-color-library db/pool team-id 1)
color-id (uuid/next)] color-id (uuid/next)]
(t/testing "upload color to library" (t/testing "upload color to library"

View file

@ -1,42 +1,40 @@
(ns uxbox.tests.test-services-files (ns uxbox.tests.test-services-files
(:require (:require
[clojure.test :as t] [clojure.test :as t]
[promesa.core :as p]
[datoteka.core :as fs] [datoteka.core :as fs]
[uxbox.common.uuid :as uuid]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media]
[uxbox.core :refer [system]]
[uxbox.http :as http] [uxbox.http :as http]
[uxbox.media :as media]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th] [uxbox.tests.helpers :as th]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]))
[uxbox.common.uuid :as uuid]
[vertx.util :as vu]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest files-crud (t/deftest files-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file-id (uuid/next) file-id (uuid/next)
page-id (uuid/next)] page-id (uuid/next)]
(t/testing "create file" (t/testing "create file"
(let [data {::sm/type :create-file (let [data {::sm/type :create-file
:profile-id (:id prof) :profile-id (:id prof)
:project-id (:id proj) :project-id proj-id
:id file-id :id file-id
:name "test file"} :name "test file"}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:name data) (:name result))) (t/is (= (:name data) (:name result)))
(t/is (= (:id proj) (:project-id result)))))) (t/is (= proj-id (:project-id result))))))
(t/testing "rename file" (t/testing "rename file"
(let [data {::sm/type :rename-file (let [data {::sm/type :rename-file
@ -52,7 +50,7 @@
(t/testing "query files" (t/testing "query files"
(let [data {::sq/type :files (let [data {::sq/type :files
:project-id (:id proj) :project-id proj-id
:profile-id (:id prof)} :profile-id (:id prof)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
@ -65,23 +63,6 @@
(t/is (= "new name" (get-in result [0 :name]))) (t/is (= "new name" (get-in result [0 :name])))
(t/is (= 1 (count (get-in result [0 :pages]))))))) (t/is (= 1 (count (get-in result [0 :pages])))))))
(t/testing "query single file with users"
(let [data {::sq/type :file-with-users
:profile-id (:id prof)
:id file-id}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= file-id (:id result)))
(t/is (= "new name" (:name result)))
(t/is (vector? (:pages result)))
(t/is (= 1 (count (:pages result))))
(t/is (vector? (:users result)))
(t/is (= (:id prof) (get-in result [:users 0 :id]))))))
(t/testing "query single file without users" (t/testing "query single file without users"
(let [data {::sq/type :file (let [data {::sq/type :file
:profile-id (:id prof) :profile-id (:id prof)
@ -128,7 +109,7 @@
(t/testing "query list files after delete" (t/testing "query list files after delete"
(let [data {::sq/type :files (let [data {::sq/type :files
:project-id (:id proj) :project-id proj-id
:profile-id (:id prof)} :profile-id (:id prof)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
@ -140,15 +121,15 @@
)) ))
(t/deftest file-images-crud (t/deftest file-images-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1)] file (th/create-file db/pool (:id prof) proj-id 1)]
(t/testing "upload file image" (t/testing "upload file image"
(let [content {:name "sample.jpg" (let [content {:filename "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg" :tempfile (th/tempfile "uxbox/tests/_files/sample.jpg")
:mtype "image/jpeg" :content-type "image/jpeg"
:size 312043} :size 312043}
data {::sm/type :upload-file-image data {::sm/type :upload-file-image
:profile-id (:id prof) :profile-id (:id prof)
@ -168,7 +149,7 @@
(t/is (= (:name data) (:name result))) (t/is (= (:name data) (:name result)))
(t/is (= (:width data) (:width result))) (t/is (= (:width data) (:width result)))
(t/is (= (:height data) (:height result))) (t/is (= (:height data) (:height result)))
(t/is (= (:mtype content) (:mtype result))) (t/is (= (:content-type content) (:mtype result)))
(t/is (string? (:path result))) (t/is (string? (:path result)))
(t/is (string? (:uri result))) (t/is (string? (:uri result)))
@ -176,12 +157,12 @@
(t/is (string? (:thumb-uri result)))))) (t/is (string? (:thumb-uri result))))))
(t/testing "import from library" (t/testing "import from library"
(let [lib @(th/create-image-library db/pool (:id team) 1) (let [lib (th/create-image-library db/pool team-id 1)
image-id (uuid/next) image-id (uuid/next)
content {:name "sample.jpg" content {:filename "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg" :tempfile (th/tempfile "uxbox/tests/_files/sample.jpg")
:mtype "image/jpeg" :content-type "image/jpeg"
:size 312043} :size 312043}
data {::sm/type :upload-image data {::sm/type :upload-image

View file

@ -1,31 +1,28 @@
(ns uxbox.tests.test-services-icons (ns uxbox.tests.test-services-icons
(:require (:require
[clojure.test :as t]
[promesa.core :as p]
[datoteka.core :as fs]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.test :as t]
[datoteka.core :as fs]
[uxbox.common.uuid :as uuid]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.core :refer [system]]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.util.storage :as ust]
[uxbox.common.uuid :as uuid]
[uxbox.tests.helpers :as th] [uxbox.tests.helpers :as th]
[vertx.core :as vc])) [uxbox.util.storage :as ust]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest icon-libraries-crud (t/deftest icon-libraries-crud
(let [id (uuid/next) (let [id (uuid/next)
prof @(th/create-profile db/pool 2) prof (th/create-profile db/pool 2)
team (:default-team prof)] team-id (:default-team prof)]
(t/testing "create library" (t/testing "create library"
(let [data {::sm/type :create-icon-library (let [data {::sm/type :create-icon-library
:name "sample library" :name "sample library"
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team) :team-id team-id
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
@ -34,25 +31,28 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= id (:id result))) (t/is (= id (:id result)))
(t/is (= (:id team) (:team-id result))) (t/is (= team-id (:team-id result)))
(t/is (= (:name data) (:name result)))))) (t/is (= (:name data) (:name result))))))
(t/testing "rename library" (t/testing "rename library"
(let [data {::sm/type :rename-icon-library (let [data {::sm/type :rename-icon-library
:name "renamed" :name "renamed"
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team) :team-id team-id
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(let [result (:result out)]
(t/is (= id (:id result)))
(t/is (= "renamed" (:name result))))))
(t/testing "query libraries" (t/testing "query libraries"
(let [data {::sq/type :icon-libraries (let [data {::sq/type :icon-libraries
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team)} :team-id team-id}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -77,7 +77,7 @@
(t/testing "query libraries after delete" (t/testing "query libraries after delete"
(let [data {::sq/type :icon-libraries (let [data {::sq/type :icon-libraries
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team)} :team-id team-id}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -88,9 +88,9 @@
)) ))
(t/deftest icons-crud (t/deftest icons-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
coll @(th/create-icon-library db/pool (:id team) 1) coll (th/create-icon-library db/pool team-id 1)
icon-id (uuid/next)] icon-id (uuid/next)]
(t/testing "upload icon to library" (t/testing "upload icon to library"

View file

@ -1,31 +1,27 @@
(ns uxbox.tests.test-services-images (ns uxbox.tests.test-services-images
(:require (:require
[clojure.test :as t] [clojure.test :as t]
[promesa.core :as p]
[datoteka.core :as fs] [datoteka.core :as fs]
[clojure.java.io :as io] [uxbox.common.uuid :as uuid]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.core :refer [system]]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.util.storage :as ust]
[uxbox.common.uuid :as uuid]
[uxbox.tests.helpers :as th] [uxbox.tests.helpers :as th]
[vertx.core :as vc])) [uxbox.util.storage :as ust]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest image-libraries-crud (t/deftest image-libraries-crud
(let [id (uuid/next) (let [id (uuid/next)
prof @(th/create-profile db/pool 2) prof (th/create-profile db/pool 2)
team (:default-team prof)] team-id (:default-team prof)]
(t/testing "create library" (t/testing "create library"
(let [data {::sm/type :create-image-library (let [data {::sm/type :create-image-library
:name "sample library" :name "sample library"
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team) :team-id team-id
:id id} :id id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
@ -33,7 +29,7 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:id team) (:team-id result))) (t/is (= team-id (:team-id result)))
(t/is (= (:name data) (:name result)))))) (t/is (= (:name data) (:name result))))))
(t/testing "rename library" (t/testing "rename library"
@ -45,7 +41,10 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(let [result (:result out)]
(t/is (= id (:id result)))
(t/is (= "renamed" (:name result))))))
(t/testing "query single library" (t/testing "query single library"
(let [data {::sq/type :image-library (let [data {::sq/type :image-library
@ -62,7 +61,7 @@
(t/testing "query libraries" (t/testing "query libraries"
(let [data {::sq/type :image-libraries (let [data {::sq/type :image-libraries
:team-id (:id team) :team-id team-id
:profile-id (:id prof)} :profile-id (:id prof)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
@ -87,7 +86,7 @@
(t/testing "query libraries after delete" (t/testing "query libraries after delete"
(let [data {::sq/type :image-libraries (let [data {::sq/type :image-libraries
:profile-id (:id prof) :profile-id (:id prof)
:team-id (:id team)} :team-id team-id}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -96,15 +95,15 @@
)) ))
(t/deftest images-crud (t/deftest images-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
lib @(th/create-image-library db/pool (:id team) 1) lib (th/create-image-library db/pool team-id 1)
image-id (uuid/next)] image-id (uuid/next)]
(t/testing "upload image to library" (t/testing "upload image to library"
(let [content {:name "sample.jpg" (let [content {:filename "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg" :tempfile (th/tempfile "uxbox/tests/_files/sample.jpg")
:mtype "image/jpeg" :content-type "image/jpeg"
:size 312043} :size 312043}
data {::sm/type :upload-image data {::sm/type :upload-image
:id image-id :id image-id
@ -127,7 +126,8 @@
(t/is (string? (get-in out [:result :path]))) (t/is (string? (get-in out [:result :path])))
(t/is (string? (get-in out [:result :thumb-path]))) (t/is (string? (get-in out [:result :thumb-path])))
(t/is (string? (get-in out [:result :uri]))) (t/is (string? (get-in out [:result :uri])))
(t/is (string? (get-in out [:result :thumb-uri]))))) (t/is (string? (get-in out [:result :thumb-uri])))
))
(t/testing "list images by library" (t/testing "list images by library"
(let [data {::sq/type :images (let [data {::sq/type :images

View file

@ -15,10 +15,10 @@
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest pages-crud (t/deftest pages-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1) file (th/create-file db/pool (:id prof) proj-id 1)
page-id (uuid/next)] page-id (uuid/next)]
(t/testing "create page" (t/testing "create page"
@ -48,7 +48,7 @@
:id page-id} :id page-id}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
(th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (string? (:share-token result)))))) (t/is (string? (:share-token result))))))
@ -93,10 +93,10 @@
)) ))
(t/deftest update-page-data (t/deftest update-page-data
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1) file (th/create-file db/pool (:id prof) proj-id 1)
page-id (uuid/next)] page-id (uuid/next)]
(t/testing "create empty page" (t/testing "create empty page"
@ -167,11 +167,11 @@
(t/deftest update-page-data-2 (t/deftest update-page-data-2
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1) file (th/create-file db/pool (:id prof) proj-id 1)
page @(th/create-page db/pool (:id prof) (:id file) 1)] page (th/create-page db/pool (:id prof) (:id file) 1)]
(t/testing "lagging changes" (t/testing "lagging changes"
(let [sid (uuid/next) (let [sid (uuid/next)
data {::sm/type :update-page data {::sm/type :update-page

View file

@ -12,7 +12,6 @@
[clojure.test :as t] [clojure.test :as t]
[clojure.java.io :as io] [clojure.java.io :as io]
[mockery.core :refer [with-mocks]] [mockery.core :refer [with-mocks]]
[promesa.core :as p]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs] [datoteka.core :as fs]
[uxbox.db :as db] [uxbox.db :as db]
@ -25,7 +24,7 @@
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest profile-login (t/deftest profile-login
(let [profile @(th/create-profile db/pool 1)] (let [profile (th/create-profile db/pool 1)]
(t/testing "failed" (t/testing "failed"
(let [event {::sm/type :login (let [event {::sm/type :login
:email "profile1.test@nodomain.com" :email "profile1.test@nodomain.com"
@ -55,8 +54,7 @@
(t/deftest profile-query-and-manipulation (t/deftest profile-query-and-manipulation
(let [profile @(th/create-profile db/pool 1)] (let [profile (th/create-profile db/pool 1)]
(t/testing "query profile" (t/testing "query profile"
(let [data {::sq/type :profile (let [data {::sq/type :profile
:profile-id (:id profile)} :profile-id (:id profile)}
@ -74,124 +72,133 @@
(let [data (assoc profile (let [data (assoc profile
::sm/type :update-profile ::sm/type :update-profile
:fullname "Full Name" :fullname "Full Name"
:name "profile222"
:lang "en" :lang "en"
:theme "dark") :theme "dark")
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:fullname data) (:fullname result)))
(t/is (= (:email data) (:email result)))
(t/is (= (:theme data) (:theme result)))
(t/is (not (contains? result :password))))))
(t/testing "update photo"
(let [data {::sm/type :update-profile-photo
:profile-id (:id profile)
:file {:name "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg"
:size 123123
:mtype "image/jpeg"}}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:id profile) (:id result))))))))
(t/deftest profile-deletion
(let [prof @(th/create-profile db/pool 1)
team (:default-team prof)
proj (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1)
page @(th/create-page db/pool (:id prof) (:id file) 1)]
(t/testing "try to delete profile not marked for deletion"
(let [params {:props {:profile-id (:id prof)}}
out (th/try-on! (uxbox.tasks.delete-profile/handler params))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))))) (t/is (nil? (:result out)))))
(t/testing "query profile after delete" (t/testing "query profile after update"
(let [data {::sq/type :profile (let [data {::sq/type :profile
:profile-id (:id prof)} :profile-id (:id profile)}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:fullname prof) (:fullname result)))))) (t/is (= "Full Name" (:fullname result)))
(t/is (= "en" (:lang result)))
(t/is (= "dark" (:theme result))))))
(t/testing "mark profile for deletion" ;; (t/testing "update photo"
(with-mocks ;; (let [data {::sm/type :update-profile-photo
[mock {:target 'uxbox.tasks/schedule! :return nil}] ;; :profile-id (:id profile)
;; :file {:name "sample.jpg"
;; :path "tests/uxbox/tests/_files/sample.jpg"
;; :size 123123
;; :mtype "image/jpeg"}}
;; out (th/try-on! (sm/handle data))]
(let [data {::sm/type :delete-profile ;; ;; (th/print-result! out)
:profile-id (:id prof)} ;; (t/is (nil? (:error out)))
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
;; check the mock ;; (let [result (:result out)]
(let [mock (deref mock) ;; (t/is (= (:id profile) (:id result))))))
mock-params (second (:call-args mock))] ))
(t/is (true? (:called? mock)))
(t/is (= 1 (:call-count mock)))
(t/is (= "delete-profile" (:name mock-params)))
(t/is (= (:id prof) (get-in mock-params [:props :profile-id]))))))
(t/testing "query files after profile soft deletion"
(let [data {::sq/type :files
:project-id (:id proj)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out))))))
(t/testing "try to delete profile marked for deletion" #_(t/deftest profile-deletion
(let [params {:props {:profile-id (:id prof)}} (let [prof (th/create-profile db/pool 1)
out (th/try-on! (uxbox.tasks.delete-profile/handler params))] team (:default-team prof)
proj (:default-project prof)
file (th/create-file db/pool (:id prof) (:id proj) 1)
page (th/create-page db/pool (:id prof) (:id file) 1)]
;; (th/print-result! out) ;; (t/testing "try to delete profile not marked for deletion"
(t/is (nil? (:error out))) ;; (let [params {:props {:profile-id (:id prof)}}
(t/is (= (:id prof) (:result out))))) ;; out (th/try-on! (uxbox.tasks.delete-profile/handler params))]
(t/testing "query profile after delete" ;; ;; (th/print-result! out)
(let [data {::sq/type :profile ;; (t/is (nil? (:error out)))
:profile-id (:id prof)} ;; (t/is (nil? (:result out)))))
out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (t/testing "query profile after delete"
;; (let [data {::sq/type :profile
;; :profile-id (:id prof)}
;; out (th/try-on! (sq/handle data))]
(let [error (:error out) ;; ;; (th/print-result! out)
error-data (ex-data error)] ;; (t/is (nil? (:error out)))
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :service-error))
(t/is (= (:name error-data) :uxbox.services.queries.profile/profile)))
(let [error (ex-cause (:error out)) ;; (let [result (:result out)]
error-data (ex-data error)] ;; (t/is (= (:fullname prof) (:fullname result))))))
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)))))
(t/testing "query files after profile permanent deletion" ;; (t/testing "mark profile for deletion"
(let [data {::sq/type :files ;; (with-mocks
:project-id (:id proj) ;; [mock {:target 'uxbox.tasks/schedule! :return nil}]
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))] ;; (let [data {::sm/type :delete-profile
;; (th/print-result! out) ;; :profile-id (:id prof)}
(t/is (nil? (:error out))) ;; out (th/try-on! (sm/handle data))]
(t/is (= 0 (count (:result out)))))))) ;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (nil? (:result out))))
;; ;; check the mock
;; (let [mock (deref mock)
;; mock-params (second (:call-args mock))]
;; (t/is (true? (:called? mock)))
;; (t/is (= 1 (:call-count mock)))
;; (t/is (= "delete-profile" (:name mock-params)))
;; (t/is (= (:id prof) (get-in mock-params [:props :profile-id]))))))
;; (t/testing "query files after profile soft deletion"
;; (let [data {::sq/type :files
;; :project-id (:id proj)
;; :profile-id (:id prof)}
;; out (th/try-on! (sq/handle data))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (= 1 (count (:result out))))))
;; (t/testing "try to delete profile marked for deletion"
;; (let [params {:props {:profile-id (:id prof)}}
;; out (th/try-on! (uxbox.tasks.delete-profile/handler params))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (= (:id prof) (:result out)))))
;; (t/testing "query profile after delete"
;; (let [data {::sq/type :profile
;; :profile-id (:id prof)}
;; out (th/try-on! (sq/handle data))]
;; ;; (th/print-result! out)
;; (let [error (:error out)
;; error-data (ex-data error)]
;; (t/is (th/ex-info? error))
;; (t/is (= (:type error-data) :service-error))
;; (t/is (= (:name error-data) :uxbox.services.queries.profile/profile)))
;; (let [error (ex-cause (:error out))
;; error-data (ex-data error)]
;; (t/is (th/ex-info? error))
;; (t/is (= (:type error-data) :not-found)))))
;; (t/testing "query files after profile permanent deletion"
;; (let [data {::sq/type :files
;; :project-id (:id proj)
;; :profile-id (:id prof)}
;; out (th/try-on! (sq/handle data))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (= 0 (count (:result out))))))
))
(t/deftest registration-domain-whitelist (t/deftest registration-domain-whitelist

View file

@ -13,8 +13,8 @@
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest projects-crud (t/deftest projects-crud
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
team @(th/create-team db/pool (:id prof) 1) team (th/create-team db/pool (:id prof) 1)
project-id (uuid/next)] project-id (uuid/next)]
(t/testing "create a project" (t/testing "create a project"
@ -51,7 +51,6 @@
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:id data) (:id result))) (t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result))) (t/is (= (:name data) (:name result)))

View file

@ -1,30 +1,27 @@
(ns uxbox.tests.test-services-viewer (ns uxbox.tests.test-services-viewer
(:require (:require
[clojure.test :as t] [clojure.test :as t]
[promesa.core :as p]
[datoteka.core :as fs] [datoteka.core :as fs]
[uxbox.common.uuid :as uuid]
[uxbox.db :as db] [uxbox.db :as db]
[uxbox.media :as media]
[uxbox.core :refer [system]]
[uxbox.http :as http] [uxbox.http :as http]
[uxbox.media :as media]
[uxbox.services.mutations :as sm] [uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq] [uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th] [uxbox.tests.helpers :as th]
[uxbox.util.storage :as ust] [uxbox.util.storage :as ust]))
[uxbox.common.uuid :as uuid]
[vertx.util :as vu]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest retrieve-bundle (t/deftest retrieve-bundle
(let [prof @(th/create-profile db/pool 1) (let [prof (th/create-profile db/pool 1)
prof2 @(th/create-profile db/pool 2) prof2 (th/create-profile db/pool 2)
team (:default-team prof) team-id (:default-team prof)
proj (:default-project prof) proj-id (:default-project prof)
file @(th/create-file db/pool (:id prof) (:id proj) 1) file (th/create-file db/pool (:id prof) proj-id 1)
page @(th/create-page db/pool (:id prof) (:id file) 1) page (th/create-page db/pool (:id prof) (:id file) 1)
token (atom nil)] token (atom nil)]

View file

@ -1,13 +0,0 @@
/target
/classes
/checkouts
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/*-init.clj
/out
/repl
/.cpcache
/.rebel*

View file

@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -1,250 +0,0 @@
# vertx-clojure
A lightweight clojure adapter for vertx toolkit.
- **STATUS**: *alpha*, in design and prototyping phase.
- **AUDIENCE**: this is not a vertx documentation, this readme intends
to explain only the clojure api
Example code on `resources/user.clj` file.
## Install
Using `deps.edn`:
```clojure
vertx-clojure/vertx {:mvn/version "0.0.0-SNAPSHOT"}
```
Using Leiningen:
```clojure
[vertx-clojure/vertx "0.0.0-SNAPSHOT"]
```
## User Guide
### Verticles
Verticles is the basic "unit of execution" in the vertx toolkit. The
concept is very simular to actors with the exception that a verticle
does not have a inbox, and verticles communicates with other verticles
or with the rest of the world using **eventbus**.
For create a verticle, you will need to create first a system instance
(a name that we give to the `Vertx` instance):
```clojure
(require '[vertx.core :as vc])
(def system (vc/system))
```
Then, you can proceed to create a verticle. A verticle concist on
three functions: `on-start`, `on-stop` and `on-error` (where the
`on-start` is mandatory the rest optional).
Lets define a dummy verticle that just prints hello world on start
callback:
```clojure
(defn on-start
[ctx]
(println "Hello world"))
(def dummy-verticle
(vc/verticle {:on-start on-start}))
```
The `dummy-verticle` is a verticle factory, nothing is running at this
momment. For run the verticle we need to deploy it using the
previously created `system` instance:
```clojure
(vc/deploy! system dummy-verticle)
```
The `deploy!` return value is a `CompletionStage` so you can deref it
like a regular `Future` or use **funcool/promesa** for chain more
complex transformations. The return value implements `AutoCloseable`
that will allow to undeploy the verticle.
The `deploy!` function also accepts an additional parameter for
options, and at this momment it only accepts as single option:
- `:instances` - number of instances to launch of the same verticle.
- `:worker` - use worker thread pool or the default event-loop.
### Event Bus
The **eventbus** is the central communication system for verticles. It
has different patterns of communication. On this documentation we will
cover the: `publish/subscribe` and `request/reply`.
Lets define a simple echo verticle:
```clojure
(require '[vertx.eventbus :as ve])
(defn on-message
[msg]
(:body msg))
(defn on-start
[ctx]
(vc/consumer ctx "test.echo" on-message))
(def echo-verticle
(vc/verticle {:on-start on-start}))
```
And then, lets deploy 4 instances of it:
```clojure
(vc/deploy! system echo-verticle {:instances 4})
```
Now, depending on how you send the messages to the "test.echo" topic,
the message will be send to a single instance of will be broadcasted
to all verticle instances subscribed to it.
To send a message and expect a response we need to use the
`ve/request!` function:
```clojure
@(ve/request! system {:foo "bar"})
;; => #vertx.eventbus.Msg{:body {:foo "bar"}}
```
The return value of `on-message` callback will be used as a reply and
it can be any plain value or a `CompletionStage`.
When you want to send a message but you don't need the return value,
there is the `ve/send!` function. And finally, if you want to send a
message to all instances subscribed to a topic, you will need to use
the `ve/publish!` function.
### Http Server (vertx.http)
**STATUS**: pre-alpha: experimental & incomplete
This part will explain the low-level api for the http server. It is
intended to be used as a building block for a more higher-level api or
when you know that you exactly need for a performance sensitive
applications.
The `vertx.http` exposes two main functions `handler` and
`server`. Lets start creating a simple "hello world" http server:
```
(require '[vertx.http :as vh])
(defn hello-world-handler
[req]
{:status 200
:body "Hello world\n"})
(defn on-start
[ctx]
(vh/server {:handler (vh/handler hello-world-handler)
:port 2021}))
(->> (vc/verticle {:on-start on-start})
(vc/deploy! system))
```
NOTE: you can start the server without creating a verticle but you
will loss the advantage of scaling (using verticle instances
parameter).
The `req` object is a plain map with the following keys:
- `:method` the HTTP method.
- `:path` the PATH of the requested URI.
- `:headers` a map with string lower-cased keys of headers.
- `:vertx.http/request` the underlying vertx request instance.
- `:vertx.http/response` the underlying vertx response instance.
And the response object to the ring response, it can contain
`:status`, `:body` and `:headers`.
**WARNING:** at this moment there are no way to obtain directly the
body of request using clojure api, this is in **design** phase and we
need to think how to expose it correctly without constraint too much
the user code (if you have suggestions, please open an issue).
**NOTE**: If you want completly bypass the clojure api, pass a vertx
`Handler` instance to server instead of using
`vertx.http/handler`. There is the `vertx.util/fn->handler` helper
that converts a plain clojure function into raw `Handler` instance.
### Web Server (vertx.web)
**STATUS**: alpha
This part will explain the higher-level http/web server api. It is a
general purpose with more clojure friendly api. It uses
`reitit-core`for the routing and `sieppari` for interceptors.
Lets start with a complete example:
```clojure
(require '[vertx.http :as vh])
(require '[vertx.web :as vw])
(require '[vertx.web.middleware :as vwm])
(defn hello-world-handler
[req]
{:status 200
:body "Hello world!\n"})
(defn on-start
[ctx]
(let [routes [["/" {:middleware [vwm/cookies]
:handler hello-world-handler
:method :get}]]
handler (vw/handler ctx
(vw/assets "/static/*" {:root "resources/public/static"})
(vw/router routes))]
(vh/server {:handler handler
:port 2022})))
(->> (vc/verticle {:on-start on-start})
(vc/deploy! system))
```
The routes are defined using `reitit-core`. The request object is very
similar to the one explained in `vertx.http`.
The main difference with `vertx.http` is that the handler is called
when the body is ready to be used and is available under `:body`
keyword on the request.
All additional features such that reading the query/form params,
parse/write cookies, cors and file uploads are provided with additional middleware
wrappers:
- `vertx.web.middleware/uploads` parses the vertx uploaded file data
structure and expose it as clojure maps under `:uploads` key.
- `vertx.web.middleware/params` parses the query string and form
params in the body if the content-type is appropriate and exposes
them under `:params`.
- `vertx.web.middleware/cors` properly sets the CORS headers.
- `vertx.web.middleware/cookies` handles the cookies reading from
the request and cookies writing from the response.
## License ##
```
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
```

View file

@ -1,35 +0,0 @@
{:deps
{org.clojure/tools.logging {:mvn/version "0.5.0"}
funcool/promesa {:mvn/version "5.0.0"}
metosin/reitit-core {:mvn/version "0.3.10"}
org.clojure/core.async {:mvn/version "1.1.587"}
io.vertx/vertx-core {:mvn/version "4.0.0-milestone4"}
io.vertx/vertx-web {:mvn/version "4.0.0-milestone4"}
io.vertx/vertx-web-client {:mvn/version "4.0.0-milestone4"}}
:paths ["src" "resources"]
:aliases
{:dev
{:extra-deps
{com.bhauman/rebel-readline {:mvn/version "0.1.4"}
metosin/jsonista {:mvn/version "0.2.5"}
mount/mount {:mvn/version "0.1.16"}
org.clojure/clojure {:mvn/version "1.10.1"}
io.netty/netty-transport-native-epoll {:mvn/version "4.1.39.Final"}
environ/environ {:mvn/version "1.1.0"}
metosin/pohjavirta {:mvn/version "0.0.1-alpha5"}
org.clojure/tools.namespace {:mvn/version "0.3.1"}}
:extra-paths ["test"]}
:repl
{:main-opts ["-m" "rebel-readline.main"]}
:jar
{:extra-deps {seancorfield/depstar {:mvn/version "0.3.4"}}
:main-opts ["-m" "hf.depstar.jar", "target/vertx.jar"]}
:ancient
{:main-opts ["-m" "deps-ancient.deps-ancient"]
:extra-deps {deps-ancient {:mvn/version "RELEASE"}}}
}}

View file

@ -1,2 +0,0 @@
#!/bin/sh
mvn deploy:deploy-file -Dfile=target/vertx.jar -DpomFile=pom.xml -DrepositoryId=clojars -Durl=https://clojars.org/repo/

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vertx-clojure</groupId>
<artifactId>vertx</artifactId>
<version>0.0.0-SNAPSHOT</version>
<name>vertx-clojure</name>
<description>Vert.x adapter for Clojure</description>
<url>https://github.com/funcool/vertx-clojure</url>
<scm>
<connection>scm:git:git://github.com/vertx-clojure/vertx.git</connection>
<developerConnection>scm:git:ssh://git@github.com/vertx-clojure/vertx.git</developerConnection>
<url>https://github.com/vertx-clojure/vertx</url>
<tag>master</tag>
</scm>
<build>
<sourceDirectory>src</sourceDirectory>
</build>
<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>tools.logging</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.0.0-milestone4</version>
</dependency>
<dependency>
<groupId>metosin</groupId>
<artifactId>reitit-core</artifactId>
<version>0.3.10</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.0.0-milestone4</version>
</dependency>
<dependency>
<groupId>funcool</groupId>
<artifactId>promesa</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
<version>4.0.0-milestone4</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>core.async</artifactId>
<version>0.7.559</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>clojars</id>
<url>https://repo.clojars.org/</url>
</repository>
</repositories>
</project>

View file

@ -1,201 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.core
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[vertx.eventbus :as vxe]
[vertx.impl :as impl])
(:import
io.vertx.core.AsyncResult
io.vertx.core.Context
io.vertx.core.DeploymentOptions
io.vertx.core.Handler
io.vertx.core.Promise
io.vertx.core.Verticle
io.vertx.core.Vertx
io.vertx.core.VertxOptions
java.util.function.Supplier))
(declare opts->deployment-options)
(declare opts->vertx-options)
(declare build-verticle)
(declare build-actor)
(declare build-disposable)
;; --- Public Api
(s/def :vertx.core$system/threads pos?)
(s/def :vertx.core$system/on-error fn?)
(s/def ::system-options
(s/keys :opt-un [:vertx.core$system/threads
:vertx.core$system/on-error]))
(defn system
"Creates a new vertx actor system instance."
([] (system {}))
([options]
(s/assert ::system-options options)
(let [^VertxOptions opts (opts->vertx-options options)
^Vertx vsm (Vertx/vertx opts)]
(vxe/configure! vsm opts)
vsm)))
(defn stop
[^Vertx o]
(.close o))
(s/def :vertx.core$verticle/on-start fn?)
(s/def :vertx.core$verticle/on-stop fn?)
(s/def :vertx.core$verticle/on-error fn?)
(s/def ::verticle-options
(s/keys :req-un [:vertx.core$verticle/on-start]
:opt-un [:vertx.core$verticle/on-stop
:vertx.core$verticle/on-error]))
(defn verticle
"Creates a verticle instance (factory)."
[options]
(s/assert ::verticle-options options)
^{::verticle true ::options options}
(reify
Supplier
(get [_] (build-verticle options))))
(defn verticle?
"Return `true` if `v` is instance of `IVerticleFactory`."
[v]
(true? (::verticle (meta v))))
(s/def :vertx.core$actor/on-message fn?)
(s/def ::actor-options
(s/keys :req-un [:vertx.core$actor/on-message]
:opt-un [:vertx.core$verticle/on-start
:vertx.core$verticle/on-error
:vertx.core$verticle/on-stop]))
(defn actor
"A shortcut for create a verticle instance (factory) that consumes a
specific topic."
[topic options]
(s/assert string? topic)
(s/assert ::actor-options options)
^{::verticle true ::options options ::topic topic}
(reify
Supplier
(get [_] (build-actor topic options))))
(s/def :vertx.core$deploy/instances pos?)
(s/def :vertx.core$deploy/worker boolean?)
(s/def ::deploy-options
(s/keys :opt-un [:vertx.core$deploy/worker
:vertx.core$deploy/instances]))
(defn deploy!
"Deploy a verticle."
([vsm supplier] (deploy! vsm supplier nil))
([vsm supplier options]
(s/assert verticle? supplier)
(s/assert ::deploy-options options)
(let [d (p/deferred)
o (opts->deployment-options options)]
(.deployVerticle ^Vertx vsm
^Supplier supplier
^DeploymentOptions o
^Handler (impl/deferred->handler d))
(p/then' d (fn [id] (build-disposable vsm id))))))
(defn undeploy!
"Undeploy the verticle, this function should be rarelly used because
the easiest way to undeplo is executin the callable returned by
`deploy!` function."
[vsm id]
(s/assert string? id)
(let [d (p/deferred)]
(.undeploy ^Vertx (impl/resolve-system vsm)
^String id
^Handler (impl/deferred->handler d))
d))
;; --- Impl
(defn- build-verticle
[{:keys [on-start on-stop on-error]
:or {on-error (constantly nil)
on-stop (constantly nil)}
:as options}]
(let [vsm (volatile! nil)
ctx (volatile! nil)
lst (volatile! nil)]
(reify Verticle
(init [_ instance context]
(vreset! vsm instance)
(vreset! ctx context))
(getVertx [_] @vsm)
(^void start [_ ^Promise o]
(-> (p/do! (on-start @ctx))
(p/handle (fn [state error]
(if error
(do
(.fail o ^Throwable error)
(on-error @ctx error))
(do
(when (map? state)
(vswap! lst merge state))
(.complete o)))))))
(^void stop [_ ^Promise o]
(p/handle (p/do! (on-stop @ctx @lst))
(fn [_ err]
(if err
(do (on-error err)
(.fail o ^Throwable err))
(.complete o))))))))
(defn- build-actor
[topic {:keys [on-message on-error on-stop on-start]
:or {on-error (constantly nil)
on-start (constantly {})
on-stop (constantly nil)}}]
(letfn [(-on-start [ctx]
(let [state (on-start ctx)
state (if (map? state) state {})
consumer (vxe/consumer ctx topic on-message)]
(assoc state ::consumer consumer)))]
(build-verticle {:on-error on-error
:on-stop on-stop
:on-start -on-start})))
(defn- build-disposable
[vsm id]
(reify
clojure.lang.IDeref
(deref [_] id)
clojure.lang.IFn
(invoke [_] (undeploy! vsm id))
java.io.Closeable
(close [_]
@(undeploy! vsm id))))
(defn- opts->deployment-options
[{:keys [instances worker]}]
(let [opts (DeploymentOptions.)]
(when instances (.setInstances opts (int instances)))
(when worker (.setWorker opts worker))
opts))
(defn- opts->vertx-options
[{:keys [threads worker-threads on-error]}]
(let [opts (VertxOptions.)]
(when threads (.setEventLoopPoolSize opts (int threads)))
(when worker-threads (.setWorkerPoolSize opts (int worker-threads)))
#_(when on-error (.exceptionHandler opts (impl/fn->handler on-error)))
opts))

View file

@ -1,125 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.eventbus
(:require [promesa.core :as p]
[vertx.impl :as impl])
(:import
io.vertx.core.Vertx
io.vertx.core.Handler
io.vertx.core.Context
io.vertx.core.eventbus.Message
io.vertx.core.eventbus.MessageConsumer
io.vertx.core.eventbus.DeliveryOptions
io.vertx.core.eventbus.EventBus
io.vertx.core.eventbus.MessageCodec
java.util.function.Supplier))
(declare opts->delivery-opts)
(declare resolve-eventbus)
(declare build-message-codec)
(declare build-message)
;; --- Public Api
(defn consumer
[vsm topic f]
(let [^EventBus bus (resolve-eventbus vsm)
^MessageConsumer consumer (.consumer bus ^String topic)]
(.handler consumer (reify Handler
(handle [_ msg]
(.pause consumer)
(-> (p/do! (f vsm (build-message msg)))
(p/handle (fn [res err]
(.resume consumer)
(.reply msg (or res err)
(opts->delivery-opts {}))))))))
(reify java.lang.AutoCloseable
(close [it]
(.unregister consumer)))))
(defn publish!
([vsm topic msg] (publish! vsm topic msg {}))
([vsm topic msg opts]
(let [bus (resolve-eventbus vsm)
opts (opts->delivery-opts opts)]
(.publish ^EventBus bus
^String topic
^Object msg
^DeliveryOptions opts)
nil)))
(defn send!
([vsm topic msg] (send! vsm topic msg {}))
([vsm topic msg opts]
(let [bus (resolve-eventbus vsm)
opts (opts->delivery-opts opts)]
(.send ^EventBus bus
^String topic
^Object msg
^DeliveryOptions opts)
nil)))
(defn request!
([vsm topic msg] (request! vsm topic msg {}))
([vsm topic msg opts]
(let [bus (resolve-eventbus vsm)
opts (opts->delivery-opts opts)
d (p/deferred)]
(.request ^EventBus bus
^String topic
^Object msg
^DeliveryOptions opts
^Handler (impl/deferred->handler d))
(p/then' d build-message))))
(defn configure!
[vsm opts]
(let [^EventBus bus (resolve-eventbus vsm)]
(.registerCodec bus (build-message-codec))))
(defrecord Msg [body])
(defn message?
[v]
(instance? Msg v))
;; --- Impl
(defn- resolve-eventbus
[o]
(cond
(instance? Vertx o) (.eventBus ^Vertx o)
(instance? Context o) (resolve-eventbus (.owner ^Context o))
(instance? EventBus o) o
:else (throw (ex-info "unexpected argument" {}))))
(defn- build-message-codec
[]
;; TODO: implement the wire encode/decode using transit+msgpack
(reify MessageCodec
(encodeToWire [_ buffer data])
(decodeFromWire [_ pos buffer])
(transform [_ data] data)
(name [_] "clj:msgpack")
(^byte systemCodecID [_] (byte -1))))
(defn- build-message
[^Message msg]
(let [metadata {::reply-to (.replyAddress msg)
::send? (.isSend msg)
::address (.address msg)}
body (.body msg)]
(Msg. body metadata nil)))
(defn- opts->delivery-opts
[{:keys [codec local?]}]
(let [^DeliveryOptions opts (DeliveryOptions.)]
(.setCodecName opts (or codec "clj:msgpack"))
(when local? (.setLocalOnly opts true))
opts))

View file

@ -1,154 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.http
"Enables `raw` access to the http facilites of vertx. If you want more
clojure idiomatic api, refer to the `vertx.web` namespace."
(:require [clojure.spec.alpha :as s]
[promesa.core :as p]
[vertx.util :as util]
[vertx.impl :as impl])
(:import
java.util.Map$Entry
clojure.lang.MapEntry
io.vertx.core.Vertx
io.vertx.core.Verticle
io.vertx.core.Handler
io.vertx.core.Future
io.vertx.core.MultiMap
io.vertx.core.Context
io.vertx.core.buffer.Buffer
io.vertx.core.http.HttpServer
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse
io.vertx.core.http.HttpServerOptions
io.vertx.core.http.ServerWebSocket))
(declare opts->http-server-options)
(declare resolve-handler)
;; --- Public Api
(declare -handle-response)
(declare -handle-body)
(defn ->headers
[^MultiMap headers]
(let [it (.iterator ^MultiMap headers)]
(loop [m (transient {})]
(if (.hasNext it)
(let [^Map$Entry me (.next it)
key (.toLowerCase ^String (.getKey me))
val (.getValue me)]
(recur (assoc! m key val)))
(persistent! m)))))
(defn- ->request
[^HttpServerRequest request]
{:method (-> request .rawMethod .toLowerCase keyword)
:path (.path request)
:headers (->headers (.headers request))
::request request
::response (.response request)})
(defn handler
[vsm f]
(reify Handler
(handle [this request]
(let [ctx (->request request)]
(-handle-response (f ctx) ctx)))))
(s/def :vertx.http/handler
(s/or :fn fn? :handler #(instance? Handler %)))
(s/def :vertx.http/host string?)
(s/def :vertx.http/port pos?)
(s/def ::server-options
(s/keys :req-un [:vertx.http/handler]
:opt-un [:vertx.http/host
:vertx.http/port]))
(defn server
"Starts a vertx http server."
[vsm {:keys [handler] :as options}]
(s/assert ::server-options options)
(let [^Vertx vsm (impl/resolve-system vsm)
^HttpServerOptions opts (opts->http-server-options options)
^HttpServer srv (.createHttpServer vsm opts)
^Handler handler (resolve-handler handler)]
(doto srv
(.requestHandler handler)
(.listen))
srv))
;; --- Impl
(defn- opts->http-server-options
[{:keys [host port]}]
(let [opts (HttpServerOptions.)]
(.setReuseAddress opts true)
(.setReusePort opts true)
(.setTcpNoDelay opts true)
(.setTcpFastOpen opts true)
(when host (.setHost opts ^String host))
(when port (.setPort opts ^int port))
opts))
(defn- resolve-handler
[handler]
(cond
(fn? handler) (impl/fn->handler handler)
(instance? Handler handler) handler
:else (throw (ex-info "invalid handler" {}))))
(defn- assign-status-and-headers!
[^HttpServerResponse res response]
(let [headers (:headers response)
status (:status response 200)]
(when (map? headers)
(util/doseq [[key val] headers]
(.putHeader res ^String (name key) ^String (str val))))
(.setStatusCode res status)))
(defprotocol IAsyncResponse
(-handle-response [_ _]))
(defprotocol IAsyncBody
(-handle-body [_ _]))
(extend-protocol IAsyncResponse
java.util.concurrent.CompletionStage
(-handle-response [data ctx]
(p/then' data #(-handle-response % ctx)))
clojure.lang.IPersistentMap
(-handle-response [data ctx]
(let [body (:body data)
res (::response ctx)]
(assign-status-and-headers! res data)
(-handle-body body res)))
nil
(-handle-response [sws ctx]))
(extend-protocol IAsyncBody
(Class/forName "[B")
(-handle-body [data res]
(.end ^HttpServerResponse res (Buffer/buffer ^bytes data)))
Buffer
(-handle-body [data res]
(.end ^HttpServerResponse res ^Buffer data))
nil
(-handle-body [data res]
(.putHeader ^HttpServerResponse res "content-length" "0")
(.end ^HttpServerResponse res))
String
(-handle-body [data res]
(let [length (count data)]
(.putHeader ^HttpServerResponse res "content-length" (str length))
(.end ^HttpServerResponse res data))))

View file

@ -1,55 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.impl
"Implementation helpers."
(:refer-clojure :exclude [doseq])
(:require [promesa.core :as p])
(:import
io.vertx.core.Vertx
io.vertx.core.Handler
io.vertx.core.Context
io.vertx.core.AsyncResult
java.util.function.Supplier))
(defn resolve-system
[o]
(cond
(instance? Vertx o) o
(instance? Context o) (.owner ^Context o)
:else (throw (ex-info "unexpected parameters" {:o o}))))
(defn fn->supplier
[f]
(reify Supplier
(get [_] (f))))
(defn fn->handler
[f]
(reify Handler
(handle [_ v]
(f v))))
(defn deferred->handler
[d]
(reify Handler
(handle [_ ar]
(if (.failed ^AsyncResult ar)
(p/reject! d (.cause ^AsyncResult ar))
(p/resolve! d (.result ^AsyncResult ar))))))
(defmacro doseq
"A faster version of doseq."
[[bsym csym] & body]
(let [itsym (gensym "iterator")]
`(let [~itsym (.iterator ~(with-meta csym {:tag 'java.lang.Iterable}))]
(loop []
(when (.hasNext ~(with-meta itsym {:tag 'java.util.Iterator}))
(let [~bsym (.next ~itsym)]
~@body
(recur)))))))

View file

@ -1,70 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns vertx.stream
"A stream abstraction on top of core.async with awareness of vertx
execution context."
(:refer-clojure :exclude [loop])
(:require
[clojure.spec.alpha :as s]
[clojure.core.async :as a]
[clojure.core :as c]
[promesa.core :as p]
[vertx.impl :as impl]
[vertx.util :as vu]))
;; --- Streams
(defmacro loop
[& args]
`(let [ctx# (vu/current-context)]
(binding [p/*loop-run-fn* #(vu/run-on-context! ctx# %)]
(p/loop ~@args))))
(defn stream
([] (a/chan))
([b] (a/chan b))
([b c] (a/chan b c))
([b c e] (a/chan b c e)))
(defn take!
[c]
(let [d (p/deferred)
ctx (vu/current-context)]
(a/take! c (fn [res]
(vu/run-on-context! ctx #(p/resolve! d res))))
d))
(defn poll!
[c]
(a/poll! c))
(defn put!
[c v]
(let [d (p/deferred)
ctx (vu/current-context)]
(a/put! c v (fn [res]
(vu/run-on-context! ctx #(p/resolve! d res))))
d))
(defn offer!
[c v]
(a/offer! c v))
(defn alts!
([ports] (alts! ports {}))
([ports opts]
(let [d (p/deferred)
ctx (vu/current-context)
deliver #(vu/run-on-context! ctx (fn [] (p/resolve! d %)))
ret (a/do-alts deliver ports opts)]
(if ret
(p/resolved @ret)
d))))
(defn close!
[c]
(a/close! c))

View file

@ -1,76 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.timers
"The timers and async scheduled tasks."
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[vertx.impl :as impl])
(:import
io.vertx.core.Vertx
io.vertx.core.Handler))
;; --- Low Level API
(defn schedule-once!
[vsm ms f]
(let [^Vertx system (impl/resolve-system vsm)
^Handler handler (impl/fn->handler (fn [v] (f)))
timer-id (.setTimer system ms handler)]
(reify
java.lang.AutoCloseable
(close [_]
(.cancelTimer system timer-id)))))
(defn schedule-periodic!
[vsm ms f]
(let [^Vertx system (impl/resolve-system vsm)
^Handler handler (impl/fn->handler (fn [v] (f)))
timer-id (.setPeriodic system ms handler)]
(reify
java.lang.AutoCloseable
(close [_]
(.cancelTimer system timer-id)))))
;; --- High Level API
(s/def ::once boolean?)
(s/def ::repeat boolean?)
(s/def ::delay integer?)
(s/def ::fn (s/or :fn fn? :var var?))
(s/def ::schedule-opts
(s/keys :req [::fn ::delay] :opt [::once ::repeat]))
(defn schedule!
"High level schedule function."
[vsm {:keys [::once ::repeat ::delay] :as opts}]
(s/assert ::schedule-opts opts)
(when (and (not once) (not repeat))
(throw (IllegalArgumentException. "you should specify `once` or `repeat` params")))
(let [system (impl/resolve-system vsm)
state (atom nil)
taskfn (fn wrapped-task []
(-> (p/do! ((::fn opts) opts))
(p/catch' (constantly nil)) ; explicitly ignore all errors
(p/then' (fn [_] ; the user needs to catch errors
(if repeat
(let [tid (schedule-once! vsm delay wrapped-task)]
(reset! state tid)
nil))
(do
(reset! state nil)
nil)))))
tid (reset! state (schedule-once! vsm delay taskfn))]
(reify
java.lang.AutoCloseable
(close [this]
(when (compare-and-set! state tid nil)
(.cancelTimer ^Vertx system tid))))))

View file

@ -1,139 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns vertx.util
(:refer-clojure :exclude [loop doseq])
(:require
[clojure.spec.alpha :as s]
[clojure.core.async :as a]
[clojure.core :as c]
[promesa.core :as p]
[vertx.impl :as impl])
(:import
io.vertx.core.AsyncResult
io.vertx.core.Context
io.vertx.core.Handler
io.vertx.core.Promise
io.vertx.core.Vertx))
(defn get-or-create-context
[vsm]
(.getOrCreateContext ^Vertx (impl/resolve-system vsm)))
(defn current-context
"Returns the current context or nil."
[]
(Vertx/currentContext))
(defmacro blocking
[& body]
(let [sym-vsm (with-meta (gensym "blocking")
{:tag 'io.vertx.core.Vertx})
sym-e (with-meta (gensym "blocking")
{:tag 'java.lang.Throwable})
sym-prm (gensym "blocking")
sym-ar (gensym "blocking")]
`(let [~sym-vsm (-> (current-context)
(impl/resolve-system))
d# (p/deferred)]
(.executeBlocking
~sym-vsm
(reify Handler
(handle [_ ~sym-prm]
(let [prm# ~(with-meta sym-prm {:tag 'io.vertx.core.Promise})]
(try
(.complete prm# (do ~@body))
(catch Throwable ~sym-e
(.fail prm# ~sym-e))))))
true
(reify Handler
(handle [_ ~sym-ar]
(let [ar# ~(with-meta sym-ar {:tag 'io.vertx.core.AsyncResult})]
(if (.failed ar#)
(p/reject! d# (.cause ar#))
(p/resolve! d# (.result ar#)))))))
d#)))
(defn wrap-blocking
([f] (wrap-blocking (current-context) f))
([ctx f]
(let [^Vertx vsm (impl/resolve-system ctx)]
(fn [& args]
(let [d (p/deferred)]
(.executeBlocking
vsm
(reify Handler
(handle [_ prm]
(try
(.complete ^Promise prm (apply f args))
(catch Throwable e
(.fail ^Promise prm e)))))
true
(reify Handler
(handle [_ ar]
(if (.failed ^AsyncResult ar)
(p/reject! d (.cause ^AsyncResult ar))
(p/resolve! d (.result ^AsyncResult ar))))))
d)))))
(defn handle-on-context
"Attaches the context (current if not explicitly provided) to the
promise execution chain."
([prm] (handle-on-context prm (current-context)))
([prm ctx]
(assert (instance? Context ctx) "`ctx` should be a valid Context instance")
(let [d (p/deferred)]
(p/finally prm (fn [v e]
(.runOnContext
^Context ctx
^Handler (reify Handler
(handle [_ v']
(if e
(p/reject! d e)
(p/resolve! d v)))))))
d)))
(defn run-on-context!
"Run callbale on context."
[ctx f]
(.runOnContext ^Context ctx
^Handler (reify Handler
(handle [_ v']
(f)))))
(defmacro loop
[& args]
`(let [ctx# (current-context)]
(binding [p/*loop-run-fn* #(run-on-context! ctx# %)]
(p/loop ~@args))))
(defmacro doseq
"A faster version of doseq."
[[bsym csym] & body]
(let [itsym (gensym "iterator")]
`(let [~itsym (.iterator ~(with-meta csym {:tag 'java.lang.Iterable}))]
(c/loop []
(when (.hasNext ~(with-meta itsym {:tag 'java.util.Iterator}))
(let [~bsym (.next ~itsym)]
~@body
(recur)))))))
(defmacro go-try
[& body]
`(a/go
(try
~@body
(catch Throwable e# e#))))
(defmacro <?
[ch]
`(let [r# (a/<! ~ch)]
(if (instance? Throwable r#)
(throw r#)
r#)))

View file

@ -1,151 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.web
"High level api for http servers."
(:require
[clojure.tools.logging :as log]
[clojure.spec.alpha :as s]
[promesa.core :as p]
[reitit.core :as rt]
[reitit.middleware :as rmw]
[vertx.http :as http]
[vertx.impl :as impl])
(:import
clojure.lang.IPersistentMap
clojure.lang.Keyword
io.vertx.core.Future
io.vertx.core.Handler
io.vertx.core.Vertx
io.vertx.core.buffer.Buffer
io.vertx.core.http.Cookie
io.vertx.core.http.HttpServer
io.vertx.core.http.HttpServerOptions
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse
io.vertx.core.http.ServerWebSocket
io.vertx.ext.web.Route
io.vertx.ext.web.Router
io.vertx.ext.web.RoutingContext
io.vertx.ext.web.handler.BodyHandler
io.vertx.ext.web.handler.LoggerHandler
io.vertx.ext.web.handler.ResponseTimeHandler
io.vertx.ext.web.handler.StaticHandler))
;; --- Public Api
(s/def ::wrap-handler
(s/or :fn fn?
:vec (s/every fn? :kind vector?)))
(defn- ->request
[^RoutingContext routing-context]
(let [^HttpServerRequest request (.request ^RoutingContext routing-context)
^HttpServerResponse response (.response ^RoutingContext routing-context)
^Vertx system (.vertx routing-context)]
{:body (.getBody routing-context)
:path (.path request)
:headers (http/->headers (.headers request))
:method (-> request .rawMethod .toLowerCase keyword)
::http/request request
::http/response response
;; ::execution-context (.getContext system)
::routing-context routing-context}))
(defn handler
"Wraps a user defined funcion based handler into a vertx-web aware
handler (with support for multipart uploads)."
[vsm & handlers]
(let [^Vertx vsm (impl/resolve-system vsm)
^Router router (Router/router vsm)]
(reduce #(%2 %1) router handlers)))
(defn assets
([path] (assets path {}))
([path {:keys [root] :or {root "public"} :as options}]
(fn [^Router router]
(let [^Route route (.route router path)
^Handler handler (doto (StaticHandler/create)
(.setCachingEnabled false)
(.setWebRoot root)
(.setDirectoryListing true))]
(.handler route handler)
;; A hack for lie to body handler that request is already handled.
(.handler route
(reify Handler
(handle [_ rc]
(.put ^RoutingContext rc "__body-handled" true)
(.next ^RoutingContext rc))))
router))))
(defn- default-handler
[ctx]
(if (::match ctx)
{:status 405}
{:status 404}))
(defn- default-on-error
[err req]
(log/error err)
{:status 500
:body "Internal server error!\n"})
(defn- router-handler
[router {:keys [path method] :as ctx}]
(if-let [{:keys [result path-params] :as match} (rt/match-by-path router path)]
(let [handler-fn (:handler result)
ctx (assoc ctx ::match match :path-params path-params)]
(handler-fn ctx))
(default-handler ctx)))
(defn router
([routes] (router routes {}))
([routes {:keys [delete-uploads?
upload-dir
on-error
log-requests?
time-response?]
:or {delete-uploads? true
upload-dir "/tmp/vertx.uploads"
on-error default-on-error
log-requests? false
time-response? true}
:as options}]
(let [rtr (rt/router routes {:compile rmw/compile-result})
rtf #(router-handler rtr %)]
(fn [^Router router]
(let [^Route route (.route router)]
(when time-response? (.handler route (ResponseTimeHandler/create)))
(when log-requests? (.handler route (LoggerHandler/create)))
(doto route
(.failureHandler
(reify Handler
(handle [_ rc]
(let [err (.failure ^RoutingContext rc)
req (.get ^RoutingContext rc "vertx$clj$req")]
(-> (p/do! (on-error err req))
(http/-handle-response req))))))
(.handler
(doto (BodyHandler/create true)
(.setDeleteUploadedFilesOnEnd delete-uploads?)
(.setUploadsDirectory upload-dir)))
(.handler
(reify Handler
(handle [_ rc]
(let [req (->request rc)
efn (fn [err]
(.put ^RoutingContext rc "vertx$clj$req" req)
(.fail ^RoutingContext rc ^Throwable err))]
(try
(let [result (rtf req)]
(-> (http/-handle-response result req)
(p/catch' efn)))
(catch Exception err
(efn err)))))))))
router))))

View file

@ -1,51 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.web.client
"High level http client."
(:refer-clojure :exclude [get])
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[reitit.core :as rt]
[vertx.http :as http]
[vertx.impl :as impl])
(:import
clojure.lang.IPersistentMap
clojure.lang.Keyword
io.vertx.core.Future
io.vertx.core.Handler
io.vertx.core.Vertx
io.vertx.core.buffer.Buffer
io.vertx.core.http.HttpMethod
io.vertx.ext.web.client.HttpRequest
io.vertx.ext.web.client.HttpResponse
io.vertx.ext.web.client.WebClientSession
io.vertx.ext.web.client.WebClient))
;; TODO: accept options
(defn create
([vsm] (create vsm {}))
([vsm opts]
(let [^Vertx system (impl/resolve-system vsm)]
(WebClient/create system))))
(defn session
[client]
(WebClientSession/create client))
(defn get
([session url] (get session url {}))
([session url opts]
(let [^HttpRequest req (.getAbs ^WebClientSession session url)
d (p/deferred)]
(.send req (impl/deferred->handler d))
(p/then d (fn [^HttpResponse res]
{:body (.bodyAsBuffer res)
:status (.statusCode res)
:headers (http/->headers (.headers res))})))))

View file

@ -1,217 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx.web.middleware
"Common middleware's."
(:require
[clojure.spec.alpha :as s]
[clojure.string :as str]
[promesa.core :as p]
[reitit.core :as r]
[vertx.http :as http]
[vertx.web :as web]
[vertx.util :as util])
(:import
clojure.lang.Keyword
clojure.lang.MapEntry
io.vertx.core.Future
io.vertx.core.Handler
io.vertx.core.MultiMap
io.vertx.core.Vertx
io.vertx.core.http.Cookie
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse
io.vertx.ext.web.FileUpload
io.vertx.ext.web.RoutingContext
java.util.Map
java.util.Map$Entry))
;; --- Cookies
(defn- build-cookie
[name data]
(cond-> (Cookie/cookie ^String name ^String (:value data))
(:http-only data) (.setHttpOnly true)
(:domain data) (.setDomain (:domain data))
(:path data) (.setPath (:path data))
(:secure data) (.setSecure true)))
(defn- handle-cookies-response
[request {:keys [cookies] :as response}]
(let [^HttpServerResponse res (::http/response request)]
(util/doseq [[key val] cookies]
(if (nil? val)
(.removeCookie res key)
(.addCookie res (build-cookie key val))))))
(defn- cookie->vector
[^Cookie item]
[(.getName item) (.getValue item)])
(defn- wrap-cookies
[handler]
(let [xf (map cookie->vector)]
(fn [request]
(let [req (::http/request request)
cookies (.cookieMap ^HttpServerRequest req)
cookies (into {} xf (vals cookies))]
(-> (p/do! (handler (assoc request :cookies cookies)))
(p/then' (fn [response]
(when (and (map? response)
(map? (:cookies response)))
(handle-cookies-response request response))
response)))))))
(def cookies
{:name ::cookies
:compile (constantly wrap-cookies)})
;; --- Params
(defn- parse-params
[^HttpServerRequest request]
(let [params (.params request)
it (.iterator ^MultiMap params)]
(loop [m (transient {})]
(if (.hasNext it)
(let [^Map$Entry o (.next it)
key (keyword (.toLowerCase ^String (.getKey o)))
prv (get m key ::default)
val (.getValue o)]
(cond
(= prv ::default)
(recur (assoc! m key val))
(vector? prv)
(recur (assoc! m key (conj prv val)))
:else
(recur (assoc! m key [prv val]))))
(persistent! m)))))
(defn- wrap-params
[handler]
(fn [request]
(let [req (::http/request request)
params (parse-params req)]
(handler (assoc request :params params)))))
(def params
{:name ::params
:compile (constantly wrap-params)})
;; --- Uploads
(defn- wrap-uploads
[handler]
(fn [request]
(let [rctx (::web/routing-context request)
uploads (.fileUploads ^RoutingContext rctx)
uploads (reduce (fn [acc ^FileUpload upload]
(assoc acc
(keyword (.name upload))
{:type :uploaded-file
:mtype (.contentType upload)
:path (.uploadedFileName upload)
:name (.fileName upload)
:size (.size upload)}))
{}
uploads)]
(handler (assoc request :uploads uploads)))))
(def uploads
{:name ::uploads
:compile (constantly wrap-uploads)})
;; --- Errors
(defn- wrap-errors
[handler on-error]
(fn [request]
(-> (p/do! (handler request))
(p/catch (fn [error]
(on-error error request))))))
(def errors
{:name ::errors
:compile (constantly wrap-errors)})
;; --- CORS
(s/def ::origin string?)
(s/def ::allow-credentials boolean?)
(s/def ::allow-methods (s/every keyword? :kind set?))
(s/def ::allow-headers (s/every keyword? :kind set?))
(s/def ::expose-headers (s/every keyword? :kind set?))
(s/def ::max-age number?)
(s/def ::cors-opts
(s/keys :req-un [::origin]
:opt-un [::allow-headers
::allow-methods
::expose-headers
::max-age]))
(defn wrap-cors
[handler opts]
(s/assert ::cors-opts opts)
(letfn [(preflight? [{:keys [method headers] :as ctx}]
(and (= method :options)
(contains? headers "origin")
(contains? headers "access-control-request-method")))
(normalize [data]
(str/join ", " (map name data)))
(allow-origin? [headers]
(let [origin (:origin opts)
value (get headers "origin")]
(cond
(nil? value) value
(= origin "*") origin
(set? origin) (origin value)
(= origin value) origin)))
(get-headers [{:keys [headers] :as ctx}]
(when-let [origin (allow-origin? headers)]
(cond-> {"access-control-allow-origin" origin
"access-control-allow-methods" "GET, OPTIONS, HEAD"}
(:allow-methods opts)
(assoc "access-control-allow-methods"
(-> (normalize (:allow-methods opts))
(str/upper-case)))
(:allow-credentials opts)
(assoc "access-control-allow-credentials" "true")
(:expose-headers opts)
(assoc "access-control-expose-headers"
(-> (normalize (:expose-headers opts))
(str/lower-case)))
(:max-age opts)
(assoc "access-control-max-age" (:max-age opts))
(:allow-headers opts)
(assoc "access-control-allow-headers"
(-> (normalize (:allow-headers opts))
(str/lower-case))))))]
(fn [request]
(if (preflight? request)
{:status 204 :headers (get-headers request)}
(-> (p/do! (handler request))
(p/then (fn [response]
(if (map? response)
(update response :headers merge (get-headers request))
response))))))))
(def cors
{:name ::cors
:compile (constantly wrap-cors)})

View file

@ -1,116 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns vertx.web.websockets
"Web Sockets."
(:require
[clojure.tools.logging :as log]
[clojure.core.async :as a]
[promesa.core :as p]
[vertx.http :as vh]
[vertx.web :as vw]
[vertx.impl :as vi]
[vertx.util :as vu]
[vertx.eventbus :as ve])
(:import
java.lang.AutoCloseable
io.vertx.core.AsyncResult
io.vertx.core.Promise
io.vertx.core.Handler
io.vertx.core.Vertx
io.vertx.core.buffer.Buffer
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse
io.vertx.core.http.ServerWebSocket))
(defrecord WebSocket [conn input output on-error]
AutoCloseable
(close [it]
(a/close! input)
(a/close! output)
(.close ^ServerWebSocket conn (short 403))))
(defn- write-to-websocket
[conn on-error message]
(let [r (a/chan 1)
h (reify Handler
(handle [_ ar]
(if (.failed ^AsyncResult ar)
(a/put! r (.cause ^AsyncResult ar))
(a/close! r))))]
(cond
(string? message)
(.writeTextMessage ^ServerWebSocket conn
^String message
^Handler h)
(instance? Buffer message)
(.writeBinaryMessage ^ServerWebSocket conn
^Buffer message
^Handler h)
:else
(a/put! r (ex-info "invalid message type" {:message message})))
r))
(defn- default-on-error
[^Throwable err]
(log/error "Unexpected exception on websocket handler:\n"
(with-out-str
(.printStackTrace err (java.io.PrintWriter. *out*)))))
(defn websocket
[{:keys [handler on-error
input-buffer-size
output-buffer-size]
:or {on-error default-on-error
input-buffer-size 64
output-buffer-size 64}}]
(reify
vh/IAsyncResponse
(-handle-response [it request]
(let [^HttpServerRequest req (::vh/request request)
^ServerWebSocket conn (.upgrade req)
inp-s (a/chan input-buffer-size)
out-s (a/chan output-buffer-size)
ctx (vu/current-context)
ws (->WebSocket conn inp-s out-s on-error)
impl-on-error
(fn [err]
(.close ^AutoCloseable ws)
(on-error err))
impl-on-close
(fn [_]
(a/close! inp-s)
(a/close! out-s))
impl-on-message
(fn [message]
(when-not (a/offer! inp-s message)
(.pause conn)
(a/put! inp-s message
(fn [res]
(when-not (false? res)
(.resume conn))))))]
(.exceptionHandler conn ^Handler (vi/fn->handler impl-on-error))
(.textMessageHandler conn ^Handler (vi/fn->handler impl-on-message))
(.closeHandler conn ^Handler (vi/fn->handler impl-on-close))
(a/go-loop []
(let [msg (a/<! out-s)]
(when-not (nil? msg)
(let [res (a/<! (write-to-websocket conn on-error msg))]
(if (instance? Throwable res)
(impl-on-error res)
(recur))))))
(vu/run-on-context! ctx #(handler ws))))))

View file

@ -1,146 +0,0 @@
(ns user
(:require
[clojure.pprint :refer [pprint]]
[clojure.test :as test]
[clojure.tools.namespace.repl :as r]
[clojure.walk :refer [macroexpand-all]]
[mount.core :as mount :refer [defstate]]
[pohjavirta.server :as pohjavirta]
[promesa.core :as p]
[reitit.core :as rt]
[jsonista.core :as j]
[vertx.core :as vc]
[vertx.eventbus :as ve]
[vertx.http :as vh]
[vertx.web :as vw])
(:import
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse))
(declare thr-name)
;; --- System
(defstate system
:start (vc/system)
:stop (.close system))
;; --- Echo Verticle (using eventbus)
(def echo-verticle*
(letfn [(on-message [ctx message]
(println (pr-str "received:" message
"on" (thr-name)
"with ctx" ctx))
(:body message))
(on-start [ctx]
(ve/consumer ctx "test.echo" on-message))]
(vc/verticle {:on-start on-start})))
(defstate echo-verticle
:start @(vc/deploy! system echo-verticle* {:instances 4}))
;; --- Echo Verticle Actor (using eventbus)
;; This is the same as the previous echo verticle, it just reduces the
;; boilerplate of creating the consumer.
;; (def echo-actor-verticle
;; (letfn [(on-message [message]
;; (println (pr-str "received:" (.body message)
;; "on" (thr-name)))
;; (.body message))]
;; (vc/actor "test.echo2" {:on-message on-message})))
;; (defstate echo-actor-verticle
;; :start @(vc/deploy! system echo-actor-verticle options))
;; --- Http Server Verticle
(def http-verticle
(letfn [(simple-handler [req]
;; (prn req)
{:status 200
:body (j/write-value-as-string
{:method (:method req)
:headers (:headers req)
:path (:path req)})})
(on-start [ctx]
(let [handler (vh/handler ctx simple-handler)]
(vh/server ctx {:handler handler :port 2020})))]
(vc/verticle {:on-start on-start})))
(defstate http-server-verticle
:start @(vc/deploy! system http-verticle {:instances 2}))
;; --- Web Router Verticle
(def web-router-verticle
(letfn [(simple-handler [req]
{:status 200
:body (j/write-value-as-string
{:method (:method req)
:path (:path req)})})
(on-start [ctx]
(let [routes [["/" {:all simple-handler}]]
handler (vw/handler ctx (vw/router routes))]
(vh/server ctx {:handler handler :port 2021})))]
(vc/verticle {:on-start on-start})))
(defstate web-server-with-router-verticle
:start @(vc/deploy! system web-router-verticle {:instances 2}))
;; --- pohjavirta
(defn handler
[req]
{:status 200
:body (j/write-value-as-string
{:method (:request-method req)
:headers (:headers req)
:path (:uri req)})})
(defstate pohjavirta-server
:start (let [instance (pohjavirta/create #'handler {:port 2022 :io-threads 2})]
(pohjavirta/start instance)
instance)
:stop (pohjavirta/stop pohjavirta-server))
;; --- Repl
(defn start
[]
(mount/start))
(defn stop
[]
(mount/stop))
(defn restart
[]
(stop)
(r/refresh :after 'user/start))
(defn- run-test
([] (run-test #"^vertx-tests.*"))
([o]
(r/refresh)
(cond
(instance? java.util.regex.Pattern o)
(test/run-all-tests o)
(symbol? o)
(if-let [sns (namespace o)]
(do (require (symbol sns))
(test/test-vars [(resolve o)]))
(test/test-ns o)))))
;; --- Helpers
(defn thr-name
[]
(.getName (Thread/currentThread)))

View file

@ -1,15 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx-tests.main
(:require [clojure.test :as t]))
(defn -main
[& args]
(let [{:keys [fail]} (t/run-all-tests #"^vertx-tests.*")]
(if (pos? fail)
(System/exit fail)
(System/exit 0))))

View file

@ -1,53 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns vertx-tests.test-core
(:require [clojure.test :as t]
[vertx.core :as vx]
[vertx.eventbus :as vxe]))
(def sleep #(Thread/sleep %))
(t/deftest start-stop-verticle
(with-open [vsm (vx/system)]
(let [state (atom {})]
(let [on-start (fn [_] (swap! state assoc :start true) {:a 1})
on-stop (fn [_ s] (swap! state assoc :stop true :inner (:a s)))
verticle (vx/verticle {:on-start on-start :on-stop on-stop})]
;; Start and stop verticle
(.close @(vx/deploy! vsm verticle))
;; Checks
(t/is (:start @state))
(t/is (:stop @state))
(t/is (= (:inner @state) 1))))))
(t/deftest start-stop-actor
(with-open [vsm (vx/system)]
(let [state (atom {})]
(let [on-start (fn [_] (swap! state assoc :start true) {:a 1})
on-stop (fn [_ s] (swap! state assoc :stop true :inner (:a s)))
rcvlock (promise)
on-message #(deliver rcvlock %2)
verticle (vx/actor "test.topic" {:on-message on-message
:on-start on-start
:on-stop on-stop})]
(with-open [vid @(vx/deploy! vsm verticle)]
;; Checks
(t/is (true? (:start @state)))
(t/is (nil? (:stop @state)))
(vxe/send! vsm "test.topic" {:num 1})
;; Checks
(t/is (vxe/message? @rcvlock))
(t/is (= {:num 1} (:body @rcvlock))))
(t/is (= (:inner @state) 1))
(t/is (true? (:stop @state)))))))

View file

@ -1,17 +0,0 @@
(require '[clojure.java.shell :as shell]
'[clojure.main])
(require '[rebel-readline.core]
'[rebel-readline.clojure.main]
'[rebel-readline.clojure.line-reader]
'[rebel-readline.clojure.service.local])
(defmulti task first)
(defmethod task :default
[args]
(let [all-tasks (-> task methods (dissoc :default) keys sort)
interposed (->> all-tasks (interpose ", ") (apply str))]
(println "Unknown or missing task. Choose one of:" interposed)
(System/exit 1)))
(task *command-line-args*)

View file

@ -39,7 +39,8 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [sid (:session-id state) (let [sid (:session-id state)
url (ws/url (str "/notifications/" file-id "/" sid))] url (ws/url "/ws/notifications" {:file-id file-id
:session-id sid})]
(assoc-in state [:ws file-id] (ws/open url)))) (assoc-in state [:ws file-id] (ws/open url))))
ptk/WatchEvent ptk/WatchEvent

View file

@ -35,13 +35,15 @@
[{:keys [body headers auth method query url response-type] [{:keys [body headers auth method query url response-type]
:or {auth true response-type :text}}] :or {auth true response-type :text}}]
(let [headers (merge {"Accept" "application/transit+json,*/*"} (let [headers (merge {"Accept" "application/transit+json,*/*"}
default-headers (when (map? body) default-headers)
headers) headers)
request {:method method request {:method method
:url url :url url
:headers headers :headers headers
:query query :query query
:body (when (not= :get method) (t/encode body))} :body (if (map? body)
(t/encode body)
body)}
options {:response-type response-type options {:response-type response-type
:credentials? auth}] :credentials? auth}]
(http/send! request options))) (http/send! request options)))

View file

@ -25,13 +25,17 @@
(-close [_] "close websocket")) (-close [_] "close websocket"))
(defn url (defn url
[path] ([path] (url path {}))
(let [url (.parse Uri cfg/url)] ([path params]
(.setPath url path) (let [uri (.parse Uri cfg/url)]
(if (= (.getScheme url) "http") (.setPath uri path)
(.setScheme url "ws") (if (= (.getScheme uri) "http")
(.setScheme url "wss")) (.setScheme uri "ws")
(.toString url))) (.setScheme uri "wss"))
(run! (fn [[k v]]
(.setParameterValue uri (name k) (str v)))
params)
(.toString uri))))
(defn open (defn open
[uri] [uri]