mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
✨ Add get-file-plugin-data RPC to read file plugin data (#10171)
The Plugin API can write arbitrary key/value plugin data onto the file root, but it could only be read back by downloading and decoding the whole file. Add a `get-file-plugin-data` read command that returns the file-level plugin data map, optionally narrowed to a single namespace, so external tooling can fetch metadata such as issue or commit references cheaply. Read-only: reuses `bfc/get-file` and `check-read-permissions!`, and adds no data-model or change-type changes. Object/page level reads are intentionally left out of this first command. Relates to #10169 Signed-off-by: Filip Sajdak <filip.sajdak@siili.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
e463e7c95f
commit
d7ccc0f3a6
@ -22,6 +22,7 @@
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uri :as uri]
|
||||
[app.config :as cf]
|
||||
@ -721,6 +722,31 @@
|
||||
(get-file-stats cfg id))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-plugin-data
|
||||
|
||||
(def ^:private schema:get-file-plugin-data
|
||||
[:map {:title "get-file-plugin-data"}
|
||||
[:id ::sm/uuid]
|
||||
[:namespace {:optional true} :keyword]])
|
||||
|
||||
(sv/defmethod ::get-file-plugin-data
|
||||
"Return the file-level plugin data: the arbitrary key/value metadata
|
||||
stored on the file root through the Plugin API. Optionally narrowed
|
||||
to a single `namespace`. Lets external tooling read metadata such as
|
||||
issue or commit references without downloading the whole file."
|
||||
{::doc/added "2.20"
|
||||
::sm/params schema:get-file-plugin-data
|
||||
::sm/result ctpg/schema:plugin-data
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id namespace]}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(let [plugin-data (-> (bfc/get-file cfg id)
|
||||
(get-in [:data :plugin-data]))]
|
||||
(if namespace
|
||||
(select-keys plugin-data [namespace])
|
||||
(or plugin-data {}))))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-libraries
|
||||
|
||||
(def ^:private schema:get-file-libraries
|
||||
|
||||
@ -2867,3 +2867,65 @@
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||
(t/deftest get-file-plugin-data
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
|
||||
;; A file without plugin data returns an empty map
|
||||
(let [out (th/command! {::th/type :get-file-plugin-data
|
||||
::rpc/profile-id (:id profile)
|
||||
:id (:id file)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= {} (:result out))))
|
||||
|
||||
;; Set file-level plugin data through a change
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :set-plugin-data
|
||||
:object-type :file
|
||||
:namespace :shared/traceability
|
||||
:key "req-id"
|
||||
:value "REQ-123"}])
|
||||
|
||||
;; The full plugin-data map is returned
|
||||
(let [out (th/command! {::th/type :get-file-plugin-data
|
||||
::rpc/profile-id (:id profile)
|
||||
:id (:id file)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= {:shared/traceability {"req-id" "REQ-123"}} (:result out))))
|
||||
|
||||
;; A namespace filter narrows the result
|
||||
(let [out (th/command! {::th/type :get-file-plugin-data
|
||||
::rpc/profile-id (:id profile)
|
||||
:id (:id file)
|
||||
:namespace :shared/traceability})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= {:shared/traceability {"req-id" "REQ-123"}} (:result out))))
|
||||
|
||||
;; A filter on an absent namespace returns an empty map
|
||||
(let [out (th/command! {::th/type :get-file-plugin-data
|
||||
::rpc/profile-id (:id profile)
|
||||
:id (:id file)
|
||||
:namespace :plugin/absent})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= {} (:result out))))))
|
||||
|
||||
(t/deftest get-file-plugin-data-forbidden
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
other (th/create-profile* 2 {:is-active true})
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id (:default-project-id owner)
|
||||
:is-shared false})
|
||||
out (th/command! {::th/type :get-file-plugin-data
|
||||
::rpc/profile-id (:id other)
|
||||
:id (:id file)})]
|
||||
|
||||
(t/is (not (nil? (:error out))))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :not-found (:type edata))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user