diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj
index 16ab4e76a..f59bd669e 100644
--- a/.clj-kondo/hooks/export.clj
+++ b/.clj-kondo/hooks/export.clj
@@ -53,24 +53,37 @@
[{:keys [:node]}]
(let [[rnode rtype ?meta & other] (:children node)
rsym (gensym (name (:k rtype)))
- result (api/list-node
- [(api/token-node (symbol "do"))
- (api/list-node
- [(api/token-node (symbol "declare"))
- (api/token-node rsym)])
- (if (= :map (:tag ?meta))
- (api/list-node
- [(api/token-node (symbol "reset-meta!"))
- (api/token-node rsym)
- ?meta])
- (api/list-node
- [(api/token-node (symbol "comment"))
- (api/token-node rsym)]))
- (api/list-node
- (into [(api/token-node (symbol "defmethod"))
- (api/token-node rsym)
- rtype]
- (cons ?meta other)))])]
- ;; (prn "==============" rtype (into {} ?meta))
+
+ [?docs other] (if (api/string-node? ?meta)
+ [?meta other]
+ [nil (cons ?meta other)])
+
+ [?meta other] (let [?meta (first other)]
+ (if (api/map-node? ?meta)
+ [?meta (rest other)]
+ [nil other]))
+
+ nodes [(api/token-node (symbol "do"))
+ (api/list-node
+ [(api/token-node (symbol "declare"))
+ (api/token-node rsym)])
+
+ (when ?docs
+ (api/list-node
+ [(api/token-node (symbol "comment")) ?docs]))
+
+ (when ?meta
+ (api/list-node
+ [(api/token-node (symbol "reset-meta!"))
+ (api/token-node rsym)
+ ?meta]))
+ (api/list-node
+ (into [(api/token-node (symbol "defmethod"))
+ (api/token-node rsym)
+ rtype]
+ other))]
+ result (api/list-node (filterv some? nodes))]
+
+ ;; (prn "=====>" rtype)
;; (prn (api/sexpr result))
{:node result}))
diff --git a/backend/deps.edn b/backend/deps.edn
index 77e325ba4..aabbb28a2 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -20,7 +20,7 @@
io.lettuce/lettuce-core {:mvn/version "6.1.8.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
- funcool/yetti {:git/tag "v9.2" :git/sha "4ddcc03"
+ funcool/yetti {:git/tag "v9.3" :git/sha "c6e2d0d"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
diff --git a/backend/resources/api-doc-entry.tmpl b/backend/resources/api-doc-entry.tmpl
new file mode 100644
index 000000000..97ce8a507
--- /dev/null
+++ b/backend/resources/api-doc-entry.tmpl
@@ -0,0 +1,54 @@
+
+
+ {#
{{item.type}}
#}
+
{{item.module}}:
+
{{item.name}}
+
+ {% if item.deprecated %}
+
+ Deprecated:
+ since v{{item.deprecated}},
+
+ {% endif %}
+
+ Auth:
+ {% if item.auth %}YES{% else %}NO{% endif %}
+
+
+
+
+
DOCSTRING:
+
+
+
+ {% if item.added %}
+ Added: on v{{item.added}}
+ {% endif %}
+
+ {% if item.deprecated %}
+ Deprecated: since v{{item.deprecated}}
+ {% endif %}
+
+ {% if item.docs %}
+ {{item.docs}}
+ {% endif %}
+
+
+ {% if item.changes %}
+
CHANGES:
+
+
+
+ {% for change in item.changes %}
+ - {{change.0}} - {{change.1}}
+ {% endfor %}
+
+
+ {% endif %}
+
+
SPEC EXPLAIN:
+
+
+
diff --git a/backend/resources/api-doc.css b/backend/resources/api-doc.css
index 2404aea93..b7b4ad5f1 100644
--- a/backend/resources/api-doc.css
+++ b/backend/resources/api-doc.css
@@ -53,7 +53,7 @@ header {
.rpc-item {
/* border: 1px solid red; */
- cursor: pointer;
+ /* cursor: pointer; */
display: flex;
flex-direction: column;
}
@@ -109,3 +109,37 @@ header {
padding: 5px 10px;
padding-bottom: 20px;
}
+
+.rpc-row-detail p {
+ font-weight: 200;
+}
+
+.rpc-row-detail p.small {
+ margin-top: 2px;
+ margin-bottom: 2px;
+ font-size: 10px;
+}
+
+.rpc-row-detail p.small {
+ margin-top: 2px;
+ margin-bottom: 2px;
+ font-size: 10px;
+}
+
+.rpc-row-detail strong {
+ font-weight: 500;
+}
+
+.rpc-row-detail .changes {
+ font-weight: 200;
+ list-style: none;
+ padding: 0px;
+}
+
+.rpc-row-detail .padded-section {
+ padding: 0px 10px;
+}
+
+p.small strong {
+ font-size: 10px;
+}
diff --git a/backend/resources/api-doc.tmpl b/backend/resources/api-doc.tmpl
index fa1a6d9ab..c7c447b4d 100644
--- a/backend/resources/api-doc.tmpl
+++ b/backend/resources/api-doc.tmpl
@@ -5,7 +5,10 @@
Builtin API Documentation - Penpot
-
+
+
+
+
@@ -16,92 +19,28 @@
- Penpot API Documentation
+ Penpot API Documentation (v{{version}})
RPC COMMAND METHODS:
{% for item in command-methods %}
- -
-
- {#
{{item.type}}
#}
-
{{item.module}}:
-
{{item.name}}
-
-
- Auth:
- {% if item.auth %}YES{% else %}NO{% endif %}
-
-
-
-
- {% if item.docs %}
-
DOCSTRING:
-
{{item.docs}}
- {% endif %}
-
-
SPEC EXPLAIN:
-
{{item.spec}}
-
-
+ {% include "api-doc-entry.tmpl" with item=item %}
{% endfor %}
RPC QUERY METHODS:
{% for item in query-methods %}
- -
-
- {#
{{item.type}}
#}
-
-
{{item.module}}:
-
{{item.name}}
-
-
- Auth:
- {% if item.auth %}YES{% else %}NO{% endif %}
-
-
-
-
- {% if item.docs %}
-
DOCSTRING:
-
{{item.docs}}
- {% endif %}
-
-
SPEC EXPLAIN:
-
{{item.spec}}
-
-
+ {% include "api-doc-entry.tmpl" with item=item %}
{% endfor %}
RPC MUTATION METHODS:
{% for item in mutation-methods %}
- -
-
- {#
{{item.type}}
#}
-
{{item.module}}:
-
{{item.name}}
-
-
- Auth:
- {% if item.auth %}YES{% else %}NO{% endif %}
-
-
-
-
- {% if item.docs %}
-
DOCSTRING:
-
{{item.docs}}
- {% endif %}
-
-
SPEC EXPLAIN:
-
{{item.spec}}
-
-
+ {% include "api-doc-entry.tmpl" with item=item %}
{% endfor %}
diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj
index 6604df6b5..8785f272e 100644
--- a/backend/src/app/db.clj
+++ b/backend/src/app/db.clj
@@ -323,9 +323,9 @@
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
(defn decode-pgarray
- ([v] (into [] (.getArray ^PgArray v)))
- ([v in] (into in (.getArray ^PgArray v)))
- ([v in xf] (into in xf (.getArray ^PgArray v))))
+ ([v] (some->> ^PgArray v .getArray vec))
+ ([v in] (some->> ^PgArray v .getArray (into in)))
+ ([v in xf] (some->> ^PgArray v .getArray (into in xf))))
(defn pgarray->set
[v]
diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj
index 2844c2d53..dd5bc35a3 100644
--- a/backend/src/app/http/awsns.clj
+++ b/backend/src/app/http/awsns.clj
@@ -16,6 +16,7 @@
[integrant.core :as ig]
[jsonista.core :as j]
[promesa.exec :as px]
+ [yetti.request :as yrq]
[yetti.response :as yrs]))
(declare parse-json)
@@ -31,9 +32,9 @@
(defmethod ig/init-key ::handler
[_ {:keys [executor] :as cfg}]
(fn [request respond _]
- (let [data (slurp (:body request))]
- (px/run! executor #(handle-request cfg data))
- (respond (yrs/response 200)))))
+ (let [data (-> request yrq/body slurp)]
+ (px/run! executor #(handle-request cfg data)))
+ (respond (yrs/response 200))))
(defn handle-request
[{:keys [http-client] :as cfg} data]
diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj
index a6a6057e6..626f5a375 100644
--- a/backend/src/app/http/middleware.clj
+++ b/backend/src/app/http/middleware.clj
@@ -37,14 +37,14 @@
(let [header (yrq/get-header request "content-type")]
(cond
(str/starts-with? header "application/transit+json")
- (with-open [is (-> request yrq/body yrq/body-stream)]
+ (with-open [is (yrq/body request)]
(let [params (t/read! (t/reader is))]
(-> request
(assoc :body-params params)
(update :params merge params))))
(str/starts-with? header "application/json")
- (with-open [is (-> request yrq/body yrq/body-stream)]
+ (with-open [is (yrq/body request)]
(let [params (json/read is)]
(-> request
(assoc :body-params params)
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index 4041340df..cc20cc7e5 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -91,9 +91,6 @@
:app.http/session
{:store (ig/ref :app.http.session/store)}
- :app.http.doc/routes
- {:methods (ig/ref :app.rpc/methods)}
-
:app.http.session/store
{:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
@@ -201,7 +198,7 @@
:tokens (ig/ref :app.tokens/tokens)
:audit-handler (ig/ref :app.loggers.audit/http-handler)
:rpc-routes (ig/ref :app.rpc/routes)
- :doc-routes (ig/ref :app.http.doc/routes)
+ :doc-routes (ig/ref :app.rpc.doc/routes)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.debug/routes
@@ -240,6 +237,9 @@
:http-client (ig/ref :app.http/client)
:executors (ig/ref :app.worker/executors)}
+ :app.rpc.doc/routes
+ {:methods (ig/ref :app.rpc/methods)}
+
:app.rpc/routes
{:methods (ig/ref :app.rpc/methods)}
diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj
index 801fc5c1f..477b0a633 100644
--- a/backend/src/app/rpc.clj
+++ b/backend/src/app/rpc.clj
@@ -241,6 +241,7 @@
[cfg]
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile
+ 'app.rpc.commands.comments
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo)
diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj
index 8d1a3bb18..8192e9f61 100644
--- a/backend/src/app/rpc/commands/auth.clj
+++ b/backend/src/app/rpc/commands/auth.clj
@@ -13,6 +13,7 @@
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
+ [app.rpc.doc :as-alias doc]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit]
@@ -133,7 +134,9 @@
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
- {:auth false ::rlimit/permits (cf/get :rlimit-password)}
+ {:auth false
+ ::rlimit/permits (cf/get :rlimit-password)
+ ::doc/added "1.15"}
[cfg params]
(login-with-password cfg params))
@@ -144,7 +147,8 @@
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
- {:auth false}
+ {:auth false
+ ::doc/added "1.15"}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
@@ -171,7 +175,9 @@
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
- {:auth false ::rlimit/permits (cf/get :rlimit-password)}
+ {:auth false
+ ::rlimit/permits (cf/get :rlimit-password)
+ ::doc/added "1.15"}
[cfg params]
(recover-profile cfg params))
@@ -224,7 +230,9 @@
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
-(sv/defmethod ::prepare-register-profile {:auth false}
+(sv/defmethod ::prepare-register-profile
+ {:auth false
+ ::doc/added "1.15"}
[cfg params]
(prepare-register cfg params))
@@ -355,7 +363,9 @@
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
- {:auth false ::rlimit/permits (cf/get :rlimit-password)}
+ {:auth false
+ ::rlimit/permits (cf/get :rlimit-password)
+ ::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
@@ -409,7 +419,9 @@
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
-(sv/defmethod ::request-profile-recovery {:auth false}
+(sv/defmethod ::request-profile-recovery
+ {:auth false
+ ::doc/added "1.15"}
[cfg params]
(request-profile-recovery cfg params))
diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj
index 94a335131..36c51b0e6 100644
--- a/backend/src/app/rpc/commands/binfile.clj
+++ b/backend/src/app/rpc/commands/binfile.clj
@@ -16,6 +16,7 @@
[app.config :as cf]
[app.db :as db]
[app.media :as media]
+ [app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.rpc.queries.projects :as projects]
[app.storage :as sto]
@@ -808,6 +809,7 @@
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
+ {::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id include-libraries? embed-assets?] :as params}]
(db/with-atomic [conn pool]
(files/check-read-permissions! conn profile-id file-id)
@@ -827,6 +829,7 @@
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format."
+ {::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id project-id file] :as params}]
(db/with-atomic [conn pool]
(projects/check-read-permissions! conn profile-id project-id)
diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj
new file mode 100644
index 000000000..0a3ef1941
--- /dev/null
+++ b/backend/src/app/rpc/commands/comments.clj
@@ -0,0 +1,202 @@
+;; 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) UXBOX Labs SL
+
+(ns app.rpc.commands.comments
+ (:require
+ [app.common.spec :as us]
+ [app.db :as db]
+ [app.rpc.doc :as-alias doc]
+ [app.rpc.queries.files :as files]
+ [app.rpc.queries.teams :as teams]
+ [app.util.services :as sv]
+ [clojure.spec.alpha :as s]))
+
+(defn decode-row
+ [{:keys [participants position] :as row}]
+ (cond-> row
+ (db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
+ (db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
+
+;; --- COMMAND: Get Comment Threads
+
+(declare retrieve-comment-threads)
+
+(s/def ::team-id ::us/uuid)
+(s/def ::file-id ::us/uuid)
+(s/def ::share-id (s/nilable ::us/uuid))
+
+(s/def ::get-comment-threads
+ (s/and (s/keys :req-un [::profile-id]
+ :opt-un [::file-id ::share-id ::team-id])
+ #(or (:file-id %) (:team-id %))))
+
+(sv/defmethod ::get-comment-threads
+ [{:keys [pool] :as cfg} params]
+ (with-open [conn (db/open pool)]
+ (retrieve-comment-threads conn params)))
+
+(def sql:comment-threads
+ "select distinct on (ct.id)
+ ct.*,
+ f.name as file_name,
+ f.project_id as project_id,
+ first_value(c.content) over w as content,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id) as count_comments,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id
+ and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
+ from comment_thread as ct
+ inner join comment as c on (c.thread_id = ct.id)
+ inner join file as f on (f.id = ct.file_id)
+ left join comment_thread_status as cts
+ on (cts.thread_id = ct.id and
+ cts.profile_id = ?)
+ where ct.file_id = ?
+ window w as (partition by c.thread_id order by c.created_at asc)")
+
+(defn retrieve-comment-threads
+ [conn {:keys [profile-id file-id share-id]}]
+ (files/check-comment-permissions! conn profile-id file-id share-id)
+ (->> (db/exec! conn [sql:comment-threads profile-id file-id])
+ (into [] (map decode-row))))
+
+;; --- COMMAND: Get Unread Comment Threads
+
+(declare retrieve-unread-comment-threads)
+
+(s/def ::team-id ::us/uuid)
+(s/def ::get-unread-comment-threads
+ (s/keys :req-un [::profile-id ::team-id]))
+
+(sv/defmethod ::get-unread-comment-threads
+ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
+ (with-open [conn (db/open pool)]
+ (teams/check-read-permissions! conn profile-id team-id)
+ (retrieve-unread-comment-threads conn params)))
+
+(def sql:comment-threads-by-team
+ "select distinct on (ct.id)
+ ct.*,
+ f.name as file_name,
+ f.project_id as project_id,
+ first_value(c.content) over w as content,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id) as count_comments,
+ (select count(1)
+ from comment as c
+ where c.thread_id = ct.id
+ and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
+ from comment_thread as ct
+ inner join comment as c on (c.thread_id = ct.id)
+ inner join file as f on (f.id = ct.file_id)
+ inner join project as p on (p.id = f.project_id)
+ left join comment_thread_status as cts
+ on (cts.thread_id = ct.id and
+ cts.profile_id = ?)
+ where p.team_id = ?
+ window w as (partition by c.thread_id order by c.created_at asc)")
+
+(def sql:unread-comment-threads-by-team
+ (str "with threads as (" sql:comment-threads-by-team ")"
+ "select * from threads where count_unread_comments > 0"))
+
+(defn retrieve-unread-comment-threads
+ [conn {:keys [profile-id team-id]}]
+ (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
+ (into [] (map decode-row))))
+
+
+;; --- COMMAND: Get Single Comment Thread
+
+(s/def ::id ::us/uuid)
+(s/def ::share-id (s/nilable ::us/uuid))
+(s/def ::get-comment-thread
+ (s/keys :req-un [::profile-id ::file-id ::id]
+ :opt-un [::share-id]))
+
+(sv/defmethod ::get-comment-thread
+ [{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}]
+ (with-open [conn (db/open pool)]
+ (files/check-comment-permissions! conn profile-id file-id share-id)
+ (let [sql (str "with threads as (" sql:comment-threads ")"
+ "select * from threads where id = ?")]
+ (-> (db/exec-one! conn [sql profile-id file-id id])
+ (decode-row)))))
+
+;; --- COMMAND: Comments
+
+(declare retrieve-comments)
+
+(s/def ::file-id ::us/uuid)
+(s/def ::share-id (s/nilable ::us/uuid))
+(s/def ::thread-id ::us/uuid)
+(s/def ::get-comments
+ (s/keys :req-un [::profile-id ::thread-id]
+ :opt-un [::share-id]))
+
+(sv/defmethod ::get-comments
+ [{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}]
+ (with-open [conn (db/open pool)]
+ (let [thread (db/get-by-id conn :comment-thread thread-id)]
+ (files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
+ (retrieve-comments conn thread-id))))
+
+(def sql:comments
+ "select c.* from comment as c
+ where c.thread_id = ?
+ order by c.created_at asc")
+
+(defn retrieve-comments
+ [conn thread-id]
+ (->> (db/exec! conn [sql:comments thread-id])
+ (into [] (map decode-row))))
+
+;; --- COMMAND: Get file comments users
+
+(declare retrieve-file-comments-users)
+
+(s/def ::file-id ::us/uuid)
+(s/def ::share-id (s/nilable ::us/uuid))
+
+(s/def ::get-profiles-for-file-comments
+ (s/keys :req-un [::profile-id ::file-id]
+ :opt-un [::share-id]))
+
+(sv/defmethod ::get-profiles-for-file-comments
+ "Retrieves a list of profiles with limited set of properties of all
+ participants on comment threads of the file."
+ {::doc/added "1.15"
+ ::doc/changes ["1.15" "Imported from queries and renamed."]}
+ [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}]
+ (with-open [conn (db/open pool)]
+ (files/check-comment-permissions! conn profile-id file-id share-id)
+ (retrieve-file-comments-users conn file-id profile-id)))
+
+;; All the profiles that had comment the file, plus the current
+;; profile.
+
+(def sql:file-comment-users
+ "WITH available_profiles AS (
+ SELECT DISTINCT owner_id AS id
+ FROM comment
+ WHERE thread_id IN (SELECT id FROM comment_thread WHERE file_id=?)
+ )
+ SELECT p.id,
+ p.email,
+ p.fullname AS name,
+ p.fullname AS fullname,
+ p.photo_id,
+ p.is_active
+ FROM profile AS p
+ WHERE p.id IN (SELECT id FROM available_profiles) OR p.id=?")
+
+(defn retrieve-file-comments-users
+ [conn file-id profile-id]
+ (db/exec! conn [sql:file-comment-users file-id profile-id]))
diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj
index 3ec8f37ae..5ba10ec40 100644
--- a/backend/src/app/rpc/commands/demo.clj
+++ b/backend/src/app/rpc/commands/demo.clj
@@ -13,6 +13,7 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.commands.auth :as cmd.auth]
+ [app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
@@ -21,7 +22,13 @@
(s/def ::create-demo-profile any?)
-(sv/defmethod ::create-demo-profile {:auth false}
+(sv/defmethod ::create-demo-profile
+ "A command that is responsible of creating a demo purpose
+ profile. It only works if the `demo-users` flag is inabled in the
+ configuration."
+ {:auth false
+ ::doc/added "1.15"
+ ::doc/changes ["1.15" "This methos is migrated from mutations to commands."]}
[{:keys [pool] :as cfg} _]
(let [id (uuid/next)
sem (System/currentTimeMillis)
diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj
index d5012058e..5c581db7b 100644
--- a/backend/src/app/rpc/commands/ldap.clj
+++ b/backend/src/app/rpc/commands/ldap.clj
@@ -12,6 +12,7 @@
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.rpc.commands.auth :as cmd.auth]
+ [app.rpc.doc :as-alias doc]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@@ -28,7 +29,11 @@
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
-(sv/defmethod ::login-with-ldap {:auth false}
+(sv/defmethod ::login-with-ldap
+ "Performs the authentication using LDAP backend. Only works if LDAP
+ is properly configured and enabled with `login-with-ldap` flag."
+ {:auth false
+ ::doc/added "1.15"}
[{:keys [session tokens ldap] :as cfg} params]
(when-not ldap
(ex/raise :type :restriction
diff --git a/backend/src/app/http/doc.clj b/backend/src/app/rpc/doc.clj
similarity index 88%
rename from backend/src/app/http/doc.clj
rename to backend/src/app/rpc/doc.clj
index c467e8644..6afec5643 100644
--- a/backend/src/app/http/doc.clj
+++ b/backend/src/app/rpc/doc.clj
@@ -4,7 +4,7 @@
;;
;; Copyright (c) UXBOX Labs SL
-(ns app.http.doc
+(ns app.rpc.doc
"API autogenerated documentation."
(:require
[app.common.data :as d]
@@ -35,10 +35,14 @@
:name (d/name name)
:module (-> (:ns mdata) (str/split ".") last)
:auth (:auth mdata true)
- :docs (::sv/docs mdata)
+ :docs (::sv/docstring mdata)
+ :deprecated (::deprecated mdata)
+ :added (::added mdata)
+ :changes (some->> (::changes mdata) (partition-all 2) (map vec))
:spec (get-spec-str (::sv/spec mdata))}))]
- {:command-methods
+ {:version (:main cf/version)
+ :command-methods
(->> (:commands methods)
(map (partial gen-doc :command))
(sort-by (juxt :module :name)))
diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj
index 45e75a626..e8a60c8b7 100644
--- a/backend/src/app/rpc/mutations/comments.clj
+++ b/backend/src/app/rpc/mutations/comments.clj
@@ -10,6 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
+ [app.rpc.doc :as-alias doc]
[app.rpc.queries.comments :as comments]
[app.rpc.queries.files :as files]
[app.rpc.retry :as retry]
@@ -37,7 +38,9 @@
(sv/defmethod ::create-comment-thread
{::retry/max-retries 3
- ::retry/matches retry/conflict-db-insert?}
+ ::retry/matches retry/conflict-db-insert?
+ ::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(files/check-comment-permissions! conn profile-id file-id share-id)
@@ -101,6 +104,8 @@
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-status
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
@@ -130,6 +135,8 @@
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
@@ -151,6 +158,8 @@
:opt-un [::share-id]))
(sv/defmethod ::add-comment
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id thread-id content share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
@@ -209,6 +218,8 @@
:opt-un [::share-id]))
(sv/defmethod ::update-comment
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id content share-id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})
@@ -242,6 +253,8 @@
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment-thread
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
@@ -258,6 +271,8 @@
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj
index a405c279c..b24544a88 100644
--- a/backend/src/app/rpc/mutations/fonts.clj
+++ b/backend/src/app/rpc/mutations/fonts.clj
@@ -13,6 +13,7 @@
[app.config :as cf]
[app.db :as db]
[app.media :as media]
+ [app.rpc.doc :as-alias doc]
[app.rpc.queries.teams :as teams]
[app.rpc.rlimit :as rlimit]
[app.storage :as sto]
@@ -151,6 +152,7 @@
(s/keys :req-un [::profile-id ::team-id ::id]))
(sv/defmethod ::delete-font-variant
+ {::doc/added "1.3"}
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
diff --git a/backend/src/app/rpc/queries/comments.clj b/backend/src/app/rpc/queries/comments.clj
index 3815ef31c..aa57d9e8d 100644
--- a/backend/src/app/rpc/queries/comments.clj
+++ b/backend/src/app/rpc/queries/comments.clj
@@ -8,6 +8,8 @@
(:require
[app.common.spec :as us]
[app.db :as db]
+ [app.rpc.commands.comments :as cmd.comments]
+ [app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
@@ -19,9 +21,7 @@
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
-;; --- Query: Comment Threads
-
-(declare retrieve-comment-threads)
+;; --- QUERY: Comment Threads
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
@@ -33,87 +33,27 @@
#(or (:file-id %) (:team-id %))))
(sv/defmethod ::comment-threads
+ {::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(with-open [conn (db/open pool)]
- (retrieve-comment-threads conn params)))
-
-(def sql:comment-threads
- "select distinct on (ct.id)
- ct.*,
- f.name as file_name,
- f.project_id as project_id,
- first_value(c.content) over w as content,
- (select count(1)
- from comment as c
- where c.thread_id = ct.id) as count_comments,
- (select count(1)
- from comment as c
- where c.thread_id = ct.id
- and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
- from comment_thread as ct
- inner join comment as c on (c.thread_id = ct.id)
- inner join file as f on (f.id = ct.file_id)
- left join comment_thread_status as cts
- on (cts.thread_id = ct.id and
- cts.profile_id = ?)
- where ct.file_id = ?
- window w as (partition by c.thread_id order by c.created_at asc)")
-
-(defn- retrieve-comment-threads
- [conn {:keys [profile-id file-id share-id]}]
- (files/check-comment-permissions! conn profile-id file-id share-id)
- (->> (db/exec! conn [sql:comment-threads profile-id file-id])
- (into [] (map decode-row))))
+ (cmd.comments/retrieve-comment-threads conn params)))
-;; --- Query: Unread Comment Threads
-
-(declare retrieve-unread-comment-threads)
+;; --- QUERY: Unread Comment Threads
(s/def ::team-id ::us/uuid)
(s/def ::unread-comment-threads
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::unread-comment-threads
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
- (retrieve-unread-comment-threads conn params)))
+ (cmd.comments/retrieve-unread-comment-threads conn params)))
-(def sql:comment-threads-by-team
- "select distinct on (ct.id)
- ct.*,
- f.name as file_name,
- f.project_id as project_id,
- first_value(c.content) over w as content,
- (select count(1)
- from comment as c
- where c.thread_id = ct.id) as count_comments,
- (select count(1)
- from comment as c
- where c.thread_id = ct.id
- and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
- from comment_thread as ct
- inner join comment as c on (c.thread_id = ct.id)
- inner join file as f on (f.id = ct.file_id)
- inner join project as p on (p.id = f.project_id)
- left join comment_thread_status as cts
- on (cts.thread_id = ct.id and
- cts.profile_id = ?)
- where p.team_id = ?
- window w as (partition by c.thread_id order by c.created_at asc)")
-
-(def sql:unread-comment-threads-by-team
- (str "with threads as (" sql:comment-threads-by-team ")"
- "select * from threads where count_unread_comments > 0"))
-
-(defn retrieve-unread-comment-threads
- [conn {:keys [profile-id team-id]}]
- (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
- (into [] (map decode-row))))
-
-
-;; --- Query: Single Comment Thread
+;; --- QUERY: Single Comment Thread
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
@@ -122,17 +62,17 @@
:opt-un [::share-id]))
(sv/defmethod ::comment-thread
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
- (let [sql (str "with threads as (" sql:comment-threads ")"
+ (let [sql (str "with threads as (" cmd.comments/sql:comment-threads ")"
"select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row)))))
-;; --- Query: Comments
-
-(declare retrieve-comments)
+;; --- QUERY: Comments
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
@@ -142,25 +82,15 @@
:opt-un [::share-id]))
(sv/defmethod ::comments
+ {::doc/added "1.0"
+ ::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
- (retrieve-comments conn thread-id))))
+ (cmd.comments/retrieve-comments conn thread-id))))
-(def sql:comments
- "select c.* from comment as c
- where c.thread_id = ?
- order by c.created_at asc")
-
-(defn- retrieve-comments
- [conn thread-id]
- (->> (db/exec! conn [sql:comments thread-id])
- (into [] (map decode-row))))
-
-;; file-comments-users
-
-(declare retrieve-file-comments-users)
+;; --- QUERY: Get file comments users
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
@@ -170,27 +100,9 @@
:opt-un [::share-id]))
(sv/defmethod ::file-comments-users
+ {::doc/deprecated "1.15"
+ ::doc/added "1.13"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
- (retrieve-file-comments-users conn file-id profile-id)))
-
-(def sql:file-comment-users
- "select p.id,
- p.email,
- p.fullname as name,
- p.fullname as fullname,
- p.photo_id,
- p.is_active
- from profile p
- where p.id in
- (select owner_id from comment
- where thread_id in
- (select id from comment_thread
- where file_id=?))
- or p.id=?
- ") ;; all the users that had comment the file, plus the current user
-
-(defn retrieve-file-comments-users
- [conn file-id profile-id]
- (db/exec! conn [sql:file-comment-users file-id profile-id]))
+ (cmd.comments/retrieve-file-comments-users conn file-id profile-id)))
diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj
index a82e08e65..681b8ef47 100644
--- a/backend/src/app/rpc/queries/viewer.clj
+++ b/backend/src/app/rpc/queries/viewer.clj
@@ -9,9 +9,9 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
- [app.rpc.queries.comments :as comments]
+ [app.rpc.commands.comments :as comments]
[app.rpc.queries.files :as files]
- [app.rpc.queries.share-link :as slnk]
+ [app.rpc.queries.share-link :as slnk]
[app.util.services :as sv]
[clojure.spec.alpha :as s]
[promesa.core :as p]))
diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj
index 532055c90..dc68c6897 100644
--- a/backend/src/app/tokens.clj
+++ b/backend/src/app/tokens.clj
@@ -18,7 +18,10 @@
(defn- generate
[cfg claims]
- (let [payload (-> claims d/without-nils t/encode)]
+ (let [payload (-> claims
+ (assoc :iat (dt/now))
+ (d/without-nils)
+ (t/encode))]
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm})))
(defn- verify
diff --git a/backend/src/app/util/services.clj b/backend/src/app/util/services.clj
index 5131f45c4..642cf27af 100644
--- a/backend/src/app/util/services.clj
+++ b/backend/src/app/util/services.clj
@@ -27,7 +27,7 @@
(throw (IllegalArgumentException. "Missing arguments on `defmethod` macro.")))
(let [mdata (assoc mdata
- ::docs (some-> docs str/<<-)
+ ::docstring (some-> docs str/<<-)
::spec sname
::name (name sname))
diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc
index 043f6a3a7..fb3563c9f 100644
--- a/common/src/app/common/pages/helpers.cljc
+++ b/common/src/app/common/pages/helpers.cljc
@@ -161,14 +161,13 @@
(defn get-frames
"Retrieves all frame objects as vector"
[objects]
- (if (contains? (meta objects) ::index-frames)
- (::index-frames (meta objects))
- (let [lookup (d/getf objects)
- xform (comp (remove #(= uuid/zero %))
- (keep lookup)
- (filter frame-shape?))]
- (->> (keys objects)
- (into [] xform)))))
+ (or (-> objects meta ::index-frames)
+ (let [lookup (d/getf objects)
+ xform (comp (remove #(= uuid/zero %))
+ (keep lookup)
+ (filter frame-shape?))]
+ (->> (keys objects)
+ (into [] xform)))))
(defn get-frames-ids
"Retrieves all frame ids as vector"
@@ -704,11 +703,10 @@
(into []
(comp (map (d/getf objects))
(if all-frames?
- identity
+ (map identity)
(remove :hide-in-viewer)))
(sort-z-index objects (get-frames-ids objects) {:top-frames? true}))))
-
(defn start-page-index
[objects]
(with-meta objects {::index-frames (get-frames (with-meta objects nil))}))
diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile
index 7299b9904..d3af5c86a 100644
--- a/docker/devenv/Dockerfile
+++ b/docker/devenv/Dockerfile
@@ -3,7 +3,7 @@ LABEL maintainer="Andrey Antukh "
ARG DEBIAN_FRONTEND=noninteractive
-ENV NODE_VERSION=v16.15.1 \
+ENV NODE_VERSION=v16.16.0 \
CLOJURE_VERSION=1.11.1.1149 \
CLJKONDO_VERSION=2022.06.22 \
BABASHKA_VERSION=0.8.156 \
@@ -57,6 +57,7 @@ RUN set -ex; \
woff-tools \
woff2 \
fontforge \
+ openssh-client \
; \
rm -rf /var/lib/apt/lists/*;
diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs
index 9694db988..2b9a2f23b 100644
--- a/frontend/src/app/main/data/workspace/shapes.cljs
+++ b/frontend/src/app/main/data/workspace/shapes.cljs
@@ -7,6 +7,7 @@
(ns app.main.data.workspace.shapes
(:require
[app.common.data :as d]
+ [app.common.data.macros :as dm]
[app.common.geom.proportions :as gpr]
[app.common.pages :as cp]
[app.common.pages.changes-builder :as pcb]
@@ -140,14 +141,15 @@
page (wsh/lookup-page state page-id)
ids (cph/clean-loops objects ids)
+ lookup (d/getf objects)
groups-to-unmask
(reduce (fn [group-ids id]
;; When the shape to delete is the mask of a masked group,
;; the mask condition must be removed, and it must be
;; converted to a normal group.
- (let [obj (get objects id)
- parent (get objects (:parent-id obj))]
+ (let [obj (lookup id)
+ parent (lookup (:parent-id obj))]
(if (and (:masked-group? parent)
(= id (first (:shapes parent))))
(conj group-ids (:id parent))
@@ -166,9 +168,11 @@
(vals objects))
;; If any of the deleted shapes is a frame with guides
- guides (into {} (map (juxt :id identity) (->> (get-in page [:options :guides])
- (vals)
- (filter #(not (contains? ids (:frame-id %)))))))
+ guides (into {}
+ (comp (map second)
+ (remove #(contains? ids (:frame-id %)))
+ (map (juxt :id identity)))
+ (dm/get-in page [:options :guides]))
starting-flows
(filter (fn [flow]
@@ -192,22 +196,18 @@
(reverse)
(into (d/ordered-set)))
- find-all-empty-parents (fn recursive-find-empty-parents [empty-parents]
- (let [all-ids (into empty-parents ids)
- empty-parents-xform
- (comp
- (map (fn [id] (get objects id)))
- (map (fn [{:keys [shapes type] :as obj}]
- (when (and (= :group type)
- (zero? (count (remove #(contains? all-ids %) shapes))))
- obj)))
- (take-while some?)
- (map :id))
- calculated-empty-parents (into #{} empty-parents-xform all-parents)]
-
- (if (= empty-parents calculated-empty-parents)
- empty-parents
- (recursive-find-empty-parents calculated-empty-parents))))
+ find-all-empty-parents
+ (fn recursive-find-empty-parents [empty-parents]
+ (let [all-ids (into empty-parents ids)
+ contains? (partial contains? all-ids)
+ xform (comp (map lookup)
+ (filter cph/group-shape?)
+ (remove #(->> (:shapes %) (remove contains?) seq))
+ (map :id))
+ parents (into #{} xform all-parents)]
+ (if (= empty-parents parents)
+ empty-parents
+ (recursive-find-empty-parents parents))))
empty-parents
;; Any parent whose children are all deleted, must be deleted too.
@@ -226,18 +226,16 @@
(assoc shape :masked-group? false)))
(pcb/update-shapes (map :id interacting-shapes)
(fn [shape]
- (update shape :interactions
- (fn [interactions]
- (when interactions
- (d/removev #(and (csi/has-destination %)
- (contains? ids (:destination %)))
- interactions))))))
- (cond->
- (seq starting-flows)
+ (d/update-when shape :interactions
+ (fn [interactions]
+ (into []
+ (remove #(and (csi/has-destination %)
+ (contains? ids (:destination %))))
+ interactions)))))
+ (cond-> (seq starting-flows)
(pcb/update-page-option :flows (fn [flows]
- (reduce #(csp/remove-flow %1 (:id %2))
- flows
- starting-flows)))))]
+ (->> (map :id starting-flows)
+ (reduce csp/remove-flow flows))))))]
(rx/of (dch/commit-changes changes))))))