Compare commits

..

2 Commits

Author SHA1 Message Date
Andrey Antukh
18f9158d43 Add better approach for error handling to obj/reify 2026-02-13 12:17:37 +01:00
Andrey Antukh
43af62e5bc Make the obj/proxy object do not extend js/Object directly 2026-02-13 08:21:37 +01:00
19 changed files with 175 additions and 289 deletions

View File

@@ -80,7 +80,7 @@ jobs:
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
working-directory: plugins
shell: bash
run: pnpm --filter ${{ inputs.plugin_name }}-plugin build
run: npx nx build ${{ inputs.plugin_name }}-plugin
- name: Select Worker name
run: |

View File

@@ -12,7 +12,6 @@ penpot - error list
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a>
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
<a class="{% if version = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a>
</div>
</nav>
<main class="horizontal-list">

View File

@@ -1,40 +0,0 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Rate Limit Report
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#context">context</a>]</div>
<div>[<a href="#result">result</a>]</div>
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD:</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="result" class="table-key">RESULT: </div>
<div class="table-val">
<pre>{{result}}</pre>
</div>
</div>
</div>
</main>
{% endblock %}

View File

@@ -3,9 +3,9 @@
{:default
[[:default :window "200000/h"]]
;; #{:main/get-teams}
;; #{:command/get-teams}
;; [[:burst :bucket "5/5/5s"]]
;; #{:main/get-profile}
;; #{:command/get-profile}
;; [[:burst :bucket "60/60/1m"]]
}

View File

@@ -240,13 +240,6 @@
(tmpl/render (-> content
(assoc :id id)
(assoc :version 4)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v5 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v5.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 5)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))]
(if-let [report (get-report request)]
@@ -254,8 +247,7 @@
1 (render-template-v1 report)
2 (render-template-v2 report)
3 (render-template-v3 report)
4 (render-template-v4 report)
5 (render-template-v5 report))]
4 (render-template-v4 report))]
{::yres/status 200
::yres/body result
::yres/headers {"content-type" "text/html; charset=utf-8"

View File

@@ -151,22 +151,20 @@
uuid/zero)
props (-> (or (::replace-props resultm)
(merge params (::props resultm)))
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))
context (merge (::context resultm)
(prepare-context-from-request request))
ip-addr (inet/parse-request request)
module (get cfg ::rpc/module)]
ip-addr (inet/parse-request request)]
{::type (or (::type resultm)
(::rpc/type cfg))
::name (or (::name resultm)
(let [sname (::sv/name mdata)]
(if (not= module "main")
(str module "-" sname)
sname)))
(::sv/name mdata))
::profile-id profile-id
::ip-addr ip-addr
::props props

View File

@@ -15,7 +15,6 @@
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -42,7 +41,7 @@
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause)))
(defn- log-record->report
(defn record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(let [data (if (concurrent-exception? cause)
@@ -87,16 +86,16 @@
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try
(let [uri (cf/get :public-uri)
report (-> record log-record->report d/without-nils)]
report (-> record record->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id (str id)
:id id
:src "logging"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 3 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- audit-event->report
(defn- event->report
[{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}]
(let [context
(reduce-kv (fn [context k v]
@@ -126,51 +125,15 @@
[{:keys [::db/pool]} {:keys [::audit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event audit-event->report d/without-nils)]
report (-> event event->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id (str id)
:id id
:src "audit-log"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 4 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- rlimit-event->report
[event]
(let [context
(-> {}
(assoc :rlimit/uid (::rlimit/uid event))
(assoc :rlimit/method (::rlimit/method event))
(assoc :backend/tenant (cf/get :tenant))
(assoc :backend/host (cf/get :host))
(assoc :backend/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version)))
result
(->> (::rlimit/results event)
(mapv (fn [result]
(-> (into (sorted-map) result)
(dissoc ::rlimit/method)))))]
{:hint (str "Rate Limit Rejection: " (::rlimit/method event) " for " (::rlimit/uid event))
:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:result (pp/pprint-str result :length 50)}))
(defn- handle-rlimit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::rlimit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event rlimit-event->report d/without-nils)]
(l/dbg :hint "registering rate limit rejection"
:id (str id)
:src "rlimit"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 5 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defmethod ig/assert-key ::reporter
[_ params]
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
@@ -191,9 +154,6 @@
(::audit/id item)
(handle-audit-event cfg item)
(::rlimit/id item)
(handle-rlimit-event cfg item)
:else
(l/warn :hint "received unexpected item" :item item))

View File

@@ -9,12 +9,10 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.config :as cf]
[app.http.client :as http]
[app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[app.util.json :as json]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -24,28 +22,21 @@
(defn- send-mattermost-notification!
[cfg {:keys [id] :as report}]
(let [type (get report :type)
text (str "#" type " | " (get report :hint) "\n"
(when id
(str (u/join (cf/get :public-uri) "/dbg/error/" id) " "))
(let [url (u/join (cf/get :public-uri) "/dbg/error/" id)
text (str "Exception: " url " "
(when-let [pid (:profile-id report)]
(if (uuid? pid)
(str "(pid: #uuid-" pid ")")
(str "(pid: #ip-" pid ")")))
(str "(pid: #uuid-" pid ")"))
"\n"
"- host: #" (:host report) "\n"
"- tenant: #" (:tenant report) "\n"
"- origin: #" (:origin report) "\n"
(when-let [href (get report :href)]
(str "- href: `" href "`\n"))
(when-let [version (get report :frontend-version)]
(str "- frontend-version: `" version "`\n"))
(when-let [version (get report :backend-version)]
(str "- backend-version: `" version "`\n"))
"- href: `" (:href report) "`\n"
"- frontend-version: `" (:frontend-version report) "`\n"
"- backend-version: `" (:backend-version report) "`\n"
"\n"
(when-let [info (:info report)]
(str "```\n" info "```"))
(when-let [trace (:trace report)]
(str "```\n"
"Trace:\n"
@@ -63,15 +54,13 @@
(l/warn :hint "error on sending data"
:response (pr-str resp)))))
(defn- log-record->report
[{:keys [::l/context ::l/id ::l/cause ::l/message] :as record}]
(defn- record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(let [public-uri (cf/get :public-uri)]
{:id id
:type "exception"
:origin "logging"
:hint (or (some-> cause ex-message) @message)
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
@@ -85,9 +74,7 @@
(defn- audit-event->report
[{:keys [::audit/context ::audit/props ::audit/id] :as event}]
{:id id
:type "exception"
:origin "audit-log"
:hint (get props :hint)
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
@@ -95,35 +82,18 @@
:profile-id (:audit/profile-id event)
:href (get props :href)})
(defn- rlimit-event->report
[event]
{:id (::rlimit/id event)
:type "notification"
:origin "rlimit"
:hint (str "rlimit reject of "
(::rlimit/method event)
" for "
(::rlimit/uid event))
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:profile-id (::rlimit/profile-id event)
:info (with-out-str
(println "Rejected by:")
(println "------------")
(println "Method: " (::rlimit/method event))
(println "Limit Name: " (::rlimit/name event))
(println "Limit Strategy:" (::rlimit/strategy event))
(println)
(println "Results & Config:")
(println "-----------------")
(doseq [result (::rlimit/results event)]
(pp/pprint (into (sorted-map) result))))})
(defn- handle-event
[cfg event event->report]
(defn- handle-log-record
[cfg record]
(try
(let [report (event->report event)]
(let [report (record->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
(defn- handle-audit-event
[cfg record]
(try
(let [report (audit-event->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
@@ -146,13 +116,10 @@
(when @enabled
(cond
(::l/id item)
(handle-event cfg item log-record->report)
(handle-log-record cfg item)
(::audit/id item)
(handle-event cfg item audit-event->report)
(::rlimit/id item)
(handle-event cfg item rlimit-event->report)
(handle-audit-event cfg item)
:else
(l/warn :hint "received unexpected item" :item item)))

View File

@@ -317,13 +317,7 @@
::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/netty-executor)
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)}
{::wrk/executor (ig/ref ::wrk/netty-executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)

View File

@@ -90,7 +90,7 @@
[methods]
(let [methods (update-vals methods peek)]
(fn [{:keys [params path-params method] :as request}]
(let [handler-name (:method-name path-params)
(let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match")
key-id (get request ::http/auth-key-id)
@@ -227,8 +227,8 @@
(wrap-authentication cfg $ mdata)))
(defn- process-method
[cfg wrap-fn [f mdata]]
(l/trc :hint "add method" :module (::module cfg) :type (::type cfg) :name (::sv/name mdata))
[cfg module wrap-fn [f mdata]]
(l/trc :hint "add method" :module module :name (::sv/name mdata))
(let [f (wrap-fn cfg f mdata)
k (keyword (::sv/name mdata))]
[k [mdata (partial f cfg)]]))
@@ -239,7 +239,7 @@
(defn- resolve-methods
[cfg]
(let [cfg (assoc cfg ::module "main" ::type "command" ::metrics-id :rpc-main-timing)]
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns
'app.rpc.commands.access-token
'app.rpc.commands.audit
@@ -266,7 +266,7 @@
'app.rpc.commands.verify-token
'app.rpc.commands.viewer
'app.rpc.commands.webhooks)
(map (partial process-method cfg wrap))
(map (partial process-method cfg "rpc" wrap))
(into {}))))
(def ^:private schema:methods-params
@@ -298,13 +298,13 @@
(defn- resolve-management-methods
[cfg]
(let [cfg (assoc cfg ::module "management" ::type "command" ::metrics-id :rpc-management-timing)
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)
mods (cond->> (list 'app.rpc.management.exporter)
(contains? cf/flags :nitrate)
(cons 'app.rpc.management.nitrate))]
(->> (apply sv/scan-ns mods)
(map (partial process-method cfg wrap-management))
(map (partial process-method cfg "management" wrap-management))
(into {}))))
(def ^:private schema:management-methods-params
@@ -359,7 +359,7 @@
(let [public-uri (cf/get :public-uri)]
["/api"
["/management"
["/methods/:method-name"
["/methods/:type"
{:middleware [[mw/shared-key-auth shared-keys]
[session/authz cfg]]
:handler (make-rpc-handler management-methods)}]
@@ -370,7 +370,7 @@
:description "MANAGEMENT API")]
["/main"
["/methods/:method-name"
["/methods/:type"
{:middleware [[mw/cors]
[sec/client-header-check]
[session/authz cfg]
@@ -388,7 +388,7 @@
["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}]
["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}]
["/rpc/command/:method-name"
["/rpc/command/:type"
{:middleware [[mw/cors]
[sec/client-header-check]
[session/authz cfg]

View File

@@ -52,8 +52,6 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.http :as-alias http]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.redis :as rds]
[app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc]
@@ -173,9 +171,9 @@
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket
[rconn profile-id now {:keys [::key ::params ::method ::capacity ::interval ::rate] :as limit}]
[rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." method "." profile-id)])
(assoc ::rscript/keys [(str key "." service "." profile-id)])
(assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
@@ -183,7 +181,7 @@
reset (* (/ (inst-ms interval) rate)
(- capacity remaining))]
(l/trace :hint "limit processed"
:method method
:service service
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
@@ -195,17 +193,17 @@
(assoc ::lresult/remaining remaining))))
(defmethod process-limit :window
[rconn uid now {:keys [::permits ::unit ::key ::method] :as limit}]
[rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}]
(let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." method "." uid "." (ct/format-inst ts))])
(assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [permits (->seconds ttl)]))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
remaining (nth result 1)]
(l/trace :hint "limit processed"
:method method
:service service
:name (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
@@ -213,13 +211,12 @@
:remaining remaining)
(-> limit
(assoc ::lresult/allowed allowed?)
(assoc ::lresult/timestamp ts)
(assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits
[{:keys [::rds/conn] :as cfg} uid limits now]
(let [results (into [] (map (partial process-limit conn uid now)) limits)
[rconn profile-id limits now]
(let [results (into [] (map (partial process-limit rconn profile-id now)) limits)
remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
@@ -230,22 +227,11 @@
rejected (d/seek (complement ::lresult/allowed) results)]
(when rejected
(let [event {::id (uuid/next)
::uid uid
::method (-> rejected ::method name)
::name (-> rejected ::name name)
::strategy (-> rejected ::strategy name)
::results results}]
(l/warn :hint "rejected rate limit"
:method (-> rejected ::method name)
:name (-> rejected ::name name)
:strategy (-> rejected ::strategy name)
:uid (str uid)
:report-id (:id event))
(loggers.mm/emit cfg event)
(loggers.db/emit cfg event)))
(l/warn :hint "rejected rate limit"
:profile-id (str profile-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
{::enabled true
::allowed (not (some? rejected))
@@ -258,7 +244,7 @@
[state skey sname]
(when-let [limits (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))]
(into [] (map #(assoc % ::method sname)) limits)))
(into [] (map #(assoc % ::service sname)) limits)))
(defn- get-uid
[{:keys [::rpc/profile-id] :as params}]
@@ -268,10 +254,10 @@
uuid/zero)))
(defn- process-request'
[cfg limits params]
[{:keys [::rds/conn] :as cfg} limits params]
(try
(let [uid (get-uid params)
result (process-limits cfg uid limits (ct/now))]
result (process-limits conn uid limits (ct/now))]
(if (contains? cf/flags :soft-rpc-rlimit)
{::enabled false}
result))
@@ -289,8 +275,8 @@
(assert (or (nil? rlimit) (valid-rlimit-instance? rlimit)) "expected a valid rlimit instance")
(if rlimit
(let [skey (keyword (::rpc/module cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/module cfg) "." (->> mdata ::sv/spec name))
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
cfg (-> cfg
(assoc ::skey skey)
(assoc ::sname sname))]

View File

@@ -33,7 +33,6 @@
java.util.Optional
java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber
software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
@@ -200,8 +199,7 @@
(defn- build-s3-client
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
(let [creds-provider (DefaultCredentialsProvider/create)
aconfig (-> (ClientAsyncConfiguration/builder)
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.build))
sconfig (-> (S3Configuration/builder)
@@ -223,7 +221,6 @@
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
builder (.credentialsProvider ^S3AsyncClientBuilder builder creds-provider)
builder (cond-> ^S3AsyncClientBuilder builder
(some? endpoint)
(.endpointOverride (URI. (str endpoint))))]
@@ -240,8 +237,7 @@
(defn- build-s3-presigner
[{:keys [::region ::endpoint]}]
(let [creds-provider (DefaultCredentialsProvider/create)
config (-> (S3Configuration/builder)
(let [config (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))]
@@ -249,7 +245,6 @@
(cond-> (some? endpoint) (.endpointOverride (URI. (str endpoint))))
(.region (lookup-region region))
(.serviceConfiguration ^S3Configuration config)
(.credentialsProvider creds-provider)
(.build))))
(defn- write-input-stream

View File

@@ -104,13 +104,13 @@
(assoc-in [::db/pool ::db/password] (:database-password config))
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(update :app.rpc/rlimit assoc
:app.loggers.mattermost/reporter nil
:app.loggers.database/reporter nil)
(update :app.rpc/methods assoc
:app.setup/templates templates
:app.loggers.mattermost/reporter nil
:app.loggers.database/reporter nil)
(update :app.rpc/methods
(fn [state]
(-> state
(assoc :app.setup/templates templates)
(assoc :app.loggers.mattermost/reporter nil)
(assoc :app.loggers.database/reporter nil))))
(dissoc :app.srepl/server
:app.http/server
:app.http/route

View File

@@ -35,7 +35,7 @@
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-font-family
[:vector ::sm/text])
[:vector :string])
(def schema:token-value-typography-map
[:map

View File

@@ -11,7 +11,6 @@
[app.common.files.helpers :as cfh]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.json :as json]
[app.common.path-names :as cpn]
[app.common.record :as crc]
[app.common.schema :as sm]
@@ -1296,7 +1295,7 @@
(get :applied-tokens))]
(reduce
(fn [acc [prop name]]
(obj/set! acc (json/write-camel-key prop) name))
(obj/set! acc (d/name prop) name))
#js {}
tokens)))}

View File

@@ -16,7 +16,7 @@
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
;; [app.plugins.shape :as shape]
[app.plugins.shape :as shape]
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
@@ -113,17 +113,13 @@
:applyToShapes
{:schema [:tuple
;; FIXME: the schema decoder is interpreting the array of shape-proxys and converting
;; them to plain maps. For now we adapt the schema to accept it, but the decoder
;; should be fixed to keep the original proxy objects coming from the plugin.
;; [:vector [:fn shape/shape-proxy?]]
[:vector [:map [:id ::sm/uuid]]]
[:maybe [:set [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]]
[:vector [:fn shape/shape-proxy?]]
[:maybe [:set ::sm/keyword]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))}
:applyToSelected
{:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
{:schema [:tuple [:maybe [:set ::sm/keyword]]]
:fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))}))

View File

@@ -10,6 +10,7 @@
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object]))
(:require
[app.common.data :as d]
[app.common.json :as json]
[app.common.schema :as sm]
[clojure.core :as c]
@@ -156,6 +157,7 @@
this-sym (with-meta (gensym (str rsym "-this-")) {:tag 'js})
target-sym (with-meta (gensym (str rsym "-target-")) {:tag 'js})
cause-sym (gensym "cause-")
make-sym
(fn [pname prefix]
@@ -176,6 +178,7 @@
wrap (c/get params :wrap)
schema-1 (c/get params :schema-1)
this? (c/get params :this false)
on-error (c/get params :on-error)
decode-expr
(c/get params :decode/fn)
@@ -214,7 +217,16 @@
(with-meta {:tag 'function}))
val-sym
(gensym (str "val-" (str/slug pname) "-"))]
(gensym (str "val-" (str/slug pname) "-"))
wrap-error-handling
(if on-error
(fn [expr]
`(try
~expr
(catch :default ~cause-sym
(~on-error ~cause-sym))))
identity)]
(concat
(when wrap
@@ -226,8 +238,13 @@
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
(.call ~fn-sym ~this-sym ~this-sym)))
get-expr)])
~(wrap-error-handling
`(.call ~fn-sym ~this-sym ~this-sym))))
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
~(wrap-error-handling
`(.call ~fn-sym ~this-sym)))))])
(when set-expr
[schema-sym schema-n
@@ -241,28 +258,35 @@
(make-sym pname "set-fn")
`(fn [~val-sym]
(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
~(wrap-error-handling
`(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym `(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym
`(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
coercer-sym `(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
val-sym (if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
coercer-sym
`(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym))))])
val-sym
(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym
`(~coercer-sym ~val-sym)]
[])]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym)))))])
(when fn-expr
[schema-sym (or schema-n schema-1)
@@ -275,7 +299,12 @@
(make-sym pname "get-fn")
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~fn-expr
~fn-sym ~(if (and (list? fn-expr)
(= 'fn (first fn-expr)))
(let [[sa sb & sother] fn-expr]
`(~sa ~sb ~(wrap-error-handling `(do ~@sother))))
fn-expr)
~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym))
@@ -284,25 +313,31 @@
;; schema-n or schema-1 is provided
~@(if (or schema-n schema-1)
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~(wrap-error-handling
`(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym
~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~schema-sym (if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~schema-sym
(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~coercer-sym (if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~coercer-sym
(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~val-sym (~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym))))]
~val-sym
(~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym)))))]
[])]
~(if wrap
`(~wrap-sym ~fn-sym)
@@ -373,14 +408,19 @@
(= :property curr)
(let [definition (first params)]
(prn definition (meta definition))
(if (some? definition)
(let [definition (if (map? definition)
(c/merge {:wrap (:wrap tmeta)} definition)
(c/merge {:wrap (:wrap tmeta)
:on-error (:on-error tmeta)}
definition)
(-> {:enumerable false}
(c/merge (meta definition))
(assoc :wrap (:wrap tmeta))
(assoc :on-error (:on-error tmeta))
(assoc :fn definition)
(dissoc :get :set)))
(dissoc :get :set :line :column)
(d/without-nils)))
definition (assoc definition :name (name ckey))]
(recur (rest params)
@@ -425,6 +465,13 @@
(let [o (get o type-symbol)]
(= o t))))
#?(:cljs
(def Proxy
(app.util.object/class
:name "Proxy"
:extends js/Object
:constructor (constantly nil))))
(defmacro reify
"A domain specific variation of reify that creates anonymous objects
on demand with the ability to assign protocol implementations and
@@ -442,7 +489,7 @@
obj-sym
(gensym "obj-")]
`(let [~obj-sym (cljs.core/js-obj)
`(let [~obj-sym (new Proxy)
~f-sym (fn [] ~type-name)]
(add-properties! ~obj-sym
{:name ~'js/Symbol.toStringTag

View File

@@ -251,14 +251,7 @@ function applyToken(
token.applyToSelected(properties);
}
// Alternative way
//
// const selection = penpot.selection;
// if (token && selection) {
// token.applyToShapes(selection, properties);
// }
// Other alternative way
// Alternatve way
//
// const selection = penpot.selection;
// if (token && selection) {

View File

@@ -3744,7 +3744,7 @@ export interface ShapeBase extends PluginData {
* and the value set to the attributes will depend on which sets are active
* (and will change if different sets or themes are activated later).
*/
readonly tokens: { [property in TokenProperty]: string };
readonly tokens: { [property: string]: string };
/**
* @return Returns true if the current shape is inside a component instance
@@ -5221,7 +5221,7 @@ type TokenDimensionProps =
| 'y'
// Stroke width
| 'strokeWidth';
| 'stroke-width';
/**
* The properties that a FontFamilies token can be applied to.