✨ 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:
Filip Sajdak 2026-10-09 18:15:07 +02:00 committed by GitHub
parent e463e7c95f
commit d7ccc0f3a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 0 deletions

View File

@ -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

View File

@ -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))))))