mirror of
https://github.com/penpot/penpot.git
synced 2025-05-31 16:16:11 +02:00
🎉 Initial work on comments subsystem.
Only workspace part; missing viewer and dashboard.
This commit is contained in:
parent
36abc4646a
commit
ca83e13802
27 changed files with 1815 additions and 172 deletions
|
@ -108,6 +108,8 @@
|
|||
{:name "0030-mod-file-table-add-missing-index"
|
||||
:fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")}
|
||||
|
||||
{:name "0031-add-conversation-related-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0031-add-conversation-related-tables.sql")}
|
||||
]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
CREATE TABLE comment_thread (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
|
||||
owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
|
||||
page_id uuid NOT NULL,
|
||||
|
||||
participants jsonb NOT NULL,
|
||||
seqn integer NOT NULL DEFAULT 0,
|
||||
|
||||
position point NOT NULL,
|
||||
|
||||
is_resolved boolean NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE INDEX comment_thread__owner_id__idx ON comment_thread(owner_id);
|
||||
CREATE UNIQUE INDEX comment_thread__file_id__seqn__idx ON comment_thread(file_id, seqn);
|
||||
|
||||
CREATE TABLE comment_thread_status (
|
||||
thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
|
||||
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
|
||||
PRIMARY KEY (thread_id, profile_id)
|
||||
);
|
||||
|
||||
CREATE TABLE comment (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
|
||||
thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
|
||||
owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
|
||||
content text NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX comment__thread_id__idx ON comment(thread_id);
|
||||
CREATE INDEX comment__owner_id__idx ON comment(owner_id);
|
||||
|
||||
ALTER TABLE file ADD COLUMN comment_thread_seqn integer DEFAULT 0;
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
(require 'app.services.queries.media)
|
||||
(require 'app.services.queries.projects)
|
||||
(require 'app.services.queries.files)
|
||||
(require 'app.services.queries.comments)
|
||||
(require 'app.services.queries.profile)
|
||||
(require 'app.services.queries.recent-files)
|
||||
(require 'app.services.queries.viewer))
|
||||
|
@ -27,6 +28,7 @@
|
|||
(require 'app.services.mutations.media)
|
||||
(require 'app.services.mutations.projects)
|
||||
(require 'app.services.mutations.files)
|
||||
(require 'app.services.mutations.comments)
|
||||
(require 'app.services.mutations.profile)
|
||||
(require 'app.services.mutations.viewer)
|
||||
(require 'app.services.mutations.verify-token))
|
||||
|
|
260
backend/src/app/services/mutations/comments.clj
Normal file
260
backend/src/app/services/mutations/comments.clj
Normal file
|
@ -0,0 +1,260 @@
|
|||
;; 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) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.services.mutations.comments
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.queries.projects :as proj]
|
||||
[app.services.queries.files :as files]
|
||||
[app.services.queries.comments :as comments]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.storage :as ust]
|
||||
[app.util.transit :as t]
|
||||
[app.util.time :as dt]))
|
||||
|
||||
;; --- Mutation: Create Comment Thread
|
||||
|
||||
(declare upsert-comment-thread-status!)
|
||||
(declare create-comment-thread)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::position ::us/point)
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
|
||||
(s/def ::create-comment-thread
|
||||
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
|
||||
|
||||
(sm/defmutation ::create-comment-thread
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(create-comment-thread conn params)))
|
||||
|
||||
(defn- retrieve-next-seqn
|
||||
[conn file-id]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(defn- create-comment-thread*
|
||||
[conn {:keys [profile-id file-id page-id position content] :as params}]
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
|
||||
thread (db/insert! conn :comment-thread
|
||||
{:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-id page-id
|
||||
:created-at now
|
||||
:modified-at now
|
||||
:seqn seqn
|
||||
:position (db/pgpoint position)})
|
||||
;; Create a comment entry
|
||||
comment (db/insert! conn :comment
|
||||
{:thread-id (:id thread)
|
||||
:owner-id profile-id
|
||||
:created-at now
|
||||
:modified-at now
|
||||
:content content})]
|
||||
|
||||
;; Make the current thread as read.
|
||||
(upsert-comment-thread-status! conn profile-id (:id thread))
|
||||
|
||||
;; Optimistic update of current seq number on file.
|
||||
(db/update! conn :file
|
||||
{:comment-thread-seqn seqn}
|
||||
{:id file-id})
|
||||
|
||||
(-> (assoc thread
|
||||
:content content
|
||||
:comment comment)
|
||||
(comments/decode-row))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn params]
|
||||
(loop [sp (db/savepoint conn)
|
||||
rc 0]
|
||||
(let [res (ex/try (create-comment-thread* conn params))]
|
||||
(cond
|
||||
(and (instance? Throwable res)
|
||||
(< rc 3))
|
||||
(do
|
||||
(db/rollback! conn sp)
|
||||
(recur (db/savepoint conn)
|
||||
(inc rc)))
|
||||
|
||||
(instance? Throwable res)
|
||||
(throw res)
|
||||
|
||||
:else res))))
|
||||
|
||||
|
||||
;; --- Mutation: Update Comment Thread Status
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
|
||||
(s/def ::update-comment-thread-status
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sm/defmutation ::update-comment-thread-status
|
||||
[{:keys [profile-id id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not cthr
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(files/check-read-permissions! conn profile-id (:file-id cthr))
|
||||
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
"insert into comment_thread_status (thread_id, profile_id)
|
||||
values (?, ?)
|
||||
on conflict (thread_id, profile_id)
|
||||
do update set modified_at = clock_timestamp()
|
||||
returning modified_at;")
|
||||
|
||||
(defn- upsert-comment-thread-status!
|
||||
[conn profile-id thread-id]
|
||||
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
|
||||
|
||||
|
||||
;; --- Mutation: Update Comment Thread
|
||||
|
||||
(s/def ::is-resolved ::us/boolean)
|
||||
(s/def ::update-comment-thread
|
||||
(s/keys :req-un [::profile-id ::id ::is-resolved]))
|
||||
|
||||
(sm/defmutation ::update-comment-thread
|
||||
[{:keys [profile-id id is-resolved] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not thread
|
||||
(ex/raise :type :not-found)
|
||||
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
nil))))
|
||||
|
||||
|
||||
;; --- Mutation: Add Comment
|
||||
|
||||
(s/def ::add-comment
|
||||
(s/keys :req-un [::profile-id ::thread-id ::content]))
|
||||
|
||||
(sm/defmutation ::add-comment
|
||||
[{:keys [profile-id thread-id content] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
|
||||
(comments/decode-row))]
|
||||
|
||||
;; Standard Checks
|
||||
(when-not thread
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
|
||||
;; NOTE: is important that all timestamptz related fields are
|
||||
;; created or updated on the database level for avoid clock
|
||||
;; inconsistencies (some user sees something read that is not
|
||||
;; read, etc...)
|
||||
(let [ppants (:participants thread #{})
|
||||
comment (db/insert! conn :comment
|
||||
{:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:content content})]
|
||||
|
||||
;; NOTE: this is done in SQL instead of using db/update!
|
||||
;; helper bacause currently the helper does not allow pass raw
|
||||
;; function call parameters to the underlying prepared
|
||||
;; statement; in a future when we fix/improve it, this can be
|
||||
;; changed to use the helper.
|
||||
|
||||
;; Update thread modified-at attribute and assoc the current
|
||||
;; profile to the participant set.
|
||||
(let [ppants (conj ppants profile-id)
|
||||
sql "update comment_thread
|
||||
set modified_at = clock_timestamp(),
|
||||
participants = ?
|
||||
where id = ?"]
|
||||
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
|
||||
|
||||
;; Update the current profile status in relation to the
|
||||
;; current thread.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id)
|
||||
|
||||
;; Return the created comment object.
|
||||
comment))))
|
||||
|
||||
|
||||
;; --- Mutation: Update Comment
|
||||
|
||||
(s/def ::update-comment
|
||||
(s/keys :req-un [::profile-id ::id ::content]))
|
||||
|
||||
(sm/defmutation ::update-comment
|
||||
[{:keys [profile-id id content] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [comment (db/get-by-id conn :comment id {:for-update true})
|
||||
_ (when-not comment (ex/raise :type :not-found))
|
||||
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
|
||||
_ (when-not thread (ex/raise :type :not-found))]
|
||||
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at (dt/now)}
|
||||
{:id (:id comment)})
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at (dt/now)}
|
||||
{:id (:id thread)})
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Delete Comment Thread
|
||||
|
||||
(s/def ::delete-comment-thread
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sm/defmutation ::delete-comment-thread
|
||||
[{:keys [profile-id id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not (= (:owner-id cthr) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil)))
|
||||
|
||||
;; --- Mutation: Delete comment
|
||||
|
||||
(s/def ::delete-comment
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sm/defmutation ::delete-comment
|
||||
[{:keys [profile-id id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [comment (db/get-by-id conn :comment id {:for-update true})]
|
||||
(when-not (= (:owner-id comment) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(db/delete! conn :comment {:id id}))))
|
|
@ -21,7 +21,7 @@
|
|||
[app.db :as db]
|
||||
[app.redis :as redis]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.mutations.projects :as proj]
|
||||
[app.services.queries.projects :as proj]
|
||||
[app.services.queries.files :as files]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.blob :as blob]
|
||||
|
@ -49,6 +49,7 @@
|
|||
(sm/defmutation ::create-file
|
||||
[{:keys [profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(create-file conn params)))
|
||||
|
||||
(defn- create-file-profile
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.queries.projects :as proj]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.blob :as blob]))
|
||||
|
||||
|
@ -25,37 +26,6 @@
|
|||
(s/def ::name ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
|
||||
;; --- Permissions Checks
|
||||
|
||||
(def ^:private sql:project-permissions
|
||||
"select tpr.is_owner,
|
||||
tpr.is_admin,
|
||||
tpr.can_edit
|
||||
from team_profile_rel as tpr
|
||||
inner join project as p on (p.team_id = tpr.team_id)
|
||||
where p.id = ?
|
||||
and tpr.profile_id = ?
|
||||
union all
|
||||
select ppr.is_owner,
|
||||
ppr.is_admin,
|
||||
ppr.can_edit
|
||||
from project_profile_rel as ppr
|
||||
where ppr.project_id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn check-edition-permissions!
|
||||
[conn profile-id project-id]
|
||||
(let [rows (db/exec! conn [sql:project-permissions
|
||||
project-id profile-id
|
||||
project-id profile-id])]
|
||||
(when (empty? rows)
|
||||
(ex/raise :type :not-found))
|
||||
(when-not (or (some :can-edit rows)
|
||||
(some :is-admin rows)
|
||||
(some :is-owner rows))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
||||
|
||||
;; --- Mutation: Create Project
|
||||
|
||||
|
@ -138,7 +108,7 @@
|
|||
[{:keys [id profile-id name] :as params}]
|
||||
(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)
|
||||
(proj/check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :project
|
||||
{:name name}
|
||||
{:id id}))))
|
||||
|
@ -153,7 +123,7 @@
|
|||
(sm/defmutation ::delete-project
|
||||
[{:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(proj/check-edition-permissions! conn profile-id id)
|
||||
|
||||
;; Schedule object deletion
|
||||
(tasks/submit! conn {:name "delete-object"
|
||||
|
|
109
backend/src/app/services/queries/comments.clj
Normal file
109
backend/src/app/services/queries/comments.clj
Normal file
|
@ -0,0 +1,109 @@
|
|||
;; 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) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.services.queries.comments
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.services.queries :as sq]
|
||||
[app.services.queries.files :as files]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.core :as fs]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(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))))
|
||||
|
||||
;; --- Query: Comment Threads
|
||||
|
||||
(declare retrieve-comment-threads)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::comment-threads
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
|
||||
(sq/defquery ::comment-threads
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(with-open [conn (db/open)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(retrieve-comment-threads conn params)))
|
||||
|
||||
(def sql:comment-threads
|
||||
"select distinct on (ct.id)
|
||||
ct.*,
|
||||
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)
|
||||
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]}]
|
||||
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
;; --- Query: Single Comment Thread
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::comment-thread
|
||||
(s/keys :req-un [::profile-id ::file-id ::id]))
|
||||
|
||||
(sq/defquery ::comment-thread
|
||||
[{:keys [profile-id file-id id] :as params}]
|
||||
(with-open [conn (db/open)]
|
||||
(files/check-read-permissions! conn profile-id file-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)))))
|
||||
|
||||
|
||||
;; --- Query: Comments
|
||||
|
||||
(declare retrieve-comments)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::thread-id ::us/uuid)
|
||||
(s/def ::comments
|
||||
(s/keys :req-un [::profile-id ::thread-id]))
|
||||
|
||||
(sq/defquery ::comments
|
||||
[{:keys [profile-id thread-id] :as params}]
|
||||
(with-open [conn (db/open)]
|
||||
(let [thread (db/get-by-id conn :comment-thread thread-id)]
|
||||
(files/check-read-permissions! conn profile-id (:file-id thread))
|
||||
(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))))
|
|
@ -33,6 +33,61 @@
|
|||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::search-term ::us/string)
|
||||
|
||||
|
||||
;; --- Query: File Permissions
|
||||
|
||||
(def ^:private sql:file-permissions
|
||||
"select fpr.is_owner,
|
||||
fpr.is_admin,
|
||||
fpr.can_edit
|
||||
from file_profile_rel as fpr
|
||||
where fpr.file_id = ?
|
||||
and fpr.profile_id = ?
|
||||
union all
|
||||
select tpr.is_owner,
|
||||
tpr.is_admin,
|
||||
tpr.can_edit
|
||||
from team_profile_rel as tpr
|
||||
inner join project as p on (p.team_id = tpr.team_id)
|
||||
inner join file as f on (p.id = f.project_id)
|
||||
where f.id = ?
|
||||
and tpr.profile_id = ?
|
||||
union all
|
||||
select ppr.is_owner,
|
||||
ppr.is_admin,
|
||||
ppr.can_edit
|
||||
from project_profile_rel as ppr
|
||||
inner join file as f on (f.project_id = ppr.project_id)
|
||||
where f.id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn check-edition-permissions!
|
||||
[conn profile-id file-id]
|
||||
(let [rows (db/exec! conn [sql:file-permissions
|
||||
file-id profile-id
|
||||
file-id profile-id
|
||||
file-id profile-id])]
|
||||
(when (empty? rows)
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(when-not (or (some :can-edit rows)
|
||||
(some :is-admin rows)
|
||||
(some :is-owner rows))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
||||
|
||||
(defn check-read-permissions!
|
||||
[conn profile-id file-id]
|
||||
(let [rows (db/exec! conn [sql:file-permissions
|
||||
file-id profile-id
|
||||
file-id profile-id
|
||||
file-id profile-id])]
|
||||
(when-not (seq rows)
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
||||
|
||||
;; --- Query: Files search
|
||||
|
||||
;; TODO: this query need to a good refactor
|
||||
|
@ -99,52 +154,8 @@
|
|||
(sq/defquery ::files
|
||||
[{:keys [profile-id project-id] :as params}]
|
||||
(with-open [conn (db/open)]
|
||||
(let [project (db/get-by-id conn :project project-id)]
|
||||
(projects/check-edition-permissions! conn profile-id project)
|
||||
(into [] decode-row-xf (db/exec! conn [sql:files project-id])))))
|
||||
|
||||
|
||||
;; --- Query: File Permissions
|
||||
|
||||
(def ^:private sql:file-permissions
|
||||
"select fpr.is_owner,
|
||||
fpr.is_admin,
|
||||
fpr.can_edit
|
||||
from file_profile_rel as fpr
|
||||
where fpr.file_id = ?
|
||||
and fpr.profile_id = ?
|
||||
union all
|
||||
select tpr.is_owner,
|
||||
tpr.is_admin,
|
||||
tpr.can_edit
|
||||
from team_profile_rel as tpr
|
||||
inner join project as p on (p.team_id = tpr.team_id)
|
||||
inner join file as f on (p.id = f.project_id)
|
||||
where f.id = ?
|
||||
and tpr.profile_id = ?
|
||||
union all
|
||||
select ppr.is_owner,
|
||||
ppr.is_admin,
|
||||
ppr.can_edit
|
||||
from project_profile_rel as ppr
|
||||
inner join file as f on (f.project_id = ppr.project_id)
|
||||
where f.id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn check-edition-permissions!
|
||||
[conn profile-id file-id]
|
||||
(let [rows (db/exec! conn [sql:file-permissions
|
||||
file-id profile-id
|
||||
file-id profile-id
|
||||
file-id profile-id])]
|
||||
(when (empty? rows)
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(when-not (or (some :can-edit rows)
|
||||
(some :is-admin rows)
|
||||
(some :is-owner rows))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
|
||||
|
||||
|
||||
;; --- Query: File (By ID)
|
||||
|
|
|
@ -18,41 +18,46 @@
|
|||
|
||||
;; --- Check Project Permissions
|
||||
|
||||
;; This SQL checks if the: (1) project is part of the team where the
|
||||
;; profile has edition permissions or (2) the profile has direct
|
||||
;; edition access granted to this project.
|
||||
|
||||
(def sql:project-permissions
|
||||
"select tp.can_edit,
|
||||
tp.is_admin,
|
||||
tp.is_owner
|
||||
from team_profile_rel as tp
|
||||
where tp.profile_id = ?
|
||||
and tp.team_id = ?
|
||||
union
|
||||
select pp.can_edit,
|
||||
pp.is_admin,
|
||||
pp.is_owner
|
||||
from project_profile_rel as pp
|
||||
where pp.profile_id = ?
|
||||
and pp.project_id = ?;")
|
||||
(def ^:private sql:project-permissions
|
||||
"select tpr.is_owner,
|
||||
tpr.is_admin,
|
||||
tpr.can_edit
|
||||
from team_profile_rel as tpr
|
||||
inner join project as p on (p.team_id = tpr.team_id)
|
||||
where p.id = ?
|
||||
and tpr.profile_id = ?
|
||||
union all
|
||||
select ppr.is_owner,
|
||||
ppr.is_admin,
|
||||
ppr.can_edit
|
||||
from project_profile_rel as ppr
|
||||
where ppr.project_id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn check-edition-permissions!
|
||||
[conn profile-id project]
|
||||
[conn profile-id project-id]
|
||||
(let [rows (db/exec! conn [sql:project-permissions
|
||||
profile-id
|
||||
(:team-id project)
|
||||
profile-id
|
||||
(:id project)])]
|
||||
project-id profile-id
|
||||
project-id profile-id])]
|
||||
(when (empty? rows)
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(when-not (or (some :can-edit rows)
|
||||
(some :is-admin rows)
|
||||
(some :is-owner rows))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
||||
(defn check-read-permissions!
|
||||
[conn profile-id project-id]
|
||||
(let [rows (db/exec! conn [sql:project-permissions
|
||||
project-id profile-id
|
||||
project-id profile-id])]
|
||||
|
||||
(when-not (seq rows)
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
||||
|
||||
|
||||
;; --- Query: Projects
|
||||
|
||||
|
@ -99,5 +104,5 @@
|
|||
[{:keys [profile-id id]}]
|
||||
(with-open [conn (db/open)]
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
(check-edition-permissions! conn profile-id project)
|
||||
(check-read-permissions! conn profile-id id)
|
||||
project)))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue