✨ Add get-file-tokens RPC to read a file's design tokens (#12172)

Clients that only need a file's design tokens, such as a CI job that
syncs them to a git repository after an update-file webhook, had to
download the whole file with get-file.

get-file-tokens returns the file's tokens as a DTCG document, the same
format the token export produces, or nil when the file has none. Active
themes and sets come from the file's tokens status, so they reflect
what is currently active.

The document is returned as a plain map. A bare TokensLib is not a
collection, so the response middleware would hand it to ring unencoded.

Relates to #7091

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Filip Sajdak 2026-10-09 20:08:14 +02:00 committed by GitHub
parent fa5e5b6176
commit 10955f17e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 87 additions and 0 deletions

View File

@ -746,6 +746,28 @@
(select-keys plugin-data [namespace])
(or plugin-data {}))))
;; --- COMMAND QUERY: get-file-tokens
(def ^:private schema:get-file-tokens
[:map {:title "get-file-tokens"}
[:file-id ::sm/uuid]])
(sv/defmethod ::get-file-tokens
"Return the design tokens of a file as a DTCG document, or nil when
the file has none. Active themes and sets come from the file's
tokens status. Cheap alternative to `get-file` when only the tokens
are needed."
{::doc/added "2.20"
::sm/params schema:get-file-tokens
::sm/result [:maybe [:map-of :string :any]]}
[cfg {:keys [::rpc/profile-id file-id]}]
(bfc/check-file-exists cfg file-id)
(check-read-permissions! cfg profile-id file-id)
(let [{:keys [data]} (bfc/get-file cfg file-id)]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(when-let [tokens-lib (cfo/get-tokens-lib data)]
(ctob/export-dtcg-json tokens-lib (cfo/get-tokens-status data))))))
;; --- COMMAND QUERY: get-file-libraries

View File

@ -2929,3 +2929,68 @@
(t/is (not (nil? (:error out))))
(let [edata (-> out :error ex-data)]
(t/is (= :not-found (:type edata))))))
(t/deftest get-file-tokens-without-tokens
(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})
out (th/command! {::th/type :get-file-tokens
::rpc/profile-id (:id profile)
:file-id (:id file)})]
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/deftest get-file-tokens-with-tokens
(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})
set-id (uuid/next)
token-id (uuid/next)]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:changes
[{:type :set-token-set
:id set-id
:attrs {:id set-id :name "brand-core"}}
{:type :set-token
:set-id set-id
:token-id token-id
:attrs {:id token-id
:name "color.primary-base"
:type :color
:value "#ff0000"}}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 1
:changes
[{:type :set-tokens-status
:theme-ids #{}
:set-ids #{set-id}}])
(let [out (th/command! {::th/type :get-file-tokens
::rpc/profile-id (:id profile)
:file-id (:id file)})
dtcg (:result out)]
(t/is (nil? (:error out)))
(t/is (= "#ff0000" (get-in dtcg ["brand-core" "color" "primary-base" "$value"])))
(t/is (= "color" (get-in dtcg ["brand-core" "color" "primary-base" "$type"])))
(t/is (= ["brand-core"] (get-in dtcg ["$metadata" "tokenSetOrder"])))
(t/is (= #{"brand-core"} (set (get-in dtcg ["$metadata" "activeSets"])))))))
(t/deftest get-file-tokens-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-tokens
::rpc/profile-id (:id other)
:file-id (:id file)})]
(t/is (th/ex-info? (:error out)))
(t/is (th/ex-of-type? (:error out) :not-found))))