mirror of
https://github.com/penpot/penpot.git
synced 2025-04-29 10:26:32 +02:00
Merge pull request #6314 from penpot/niwinz-subscriptions-internal-api
✨ Add prepl api for subscriptions
This commit is contained in:
commit
1f0644ea91
5 changed files with 316 additions and 68 deletions
|
@ -35,40 +35,35 @@ def get_prepl_conninfo():
|
||||||
|
|
||||||
return host, port
|
return host, port
|
||||||
|
|
||||||
def send_eval(expr):
|
def send(data):
|
||||||
host, port = get_prepl_conninfo()
|
host, port = get_prepl_conninfo()
|
||||||
|
with socket.create_connection((host, port)) as s:
|
||||||
|
f = s.makefile(mode="rw")
|
||||||
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
json.dump(data, f)
|
||||||
s.connect((host, port))
|
f.write("\n")
|
||||||
s.send(expr.encode("utf-8"))
|
f.flush()
|
||||||
s.send(b":repl/quit\n\n")
|
|
||||||
|
|
||||||
with s.makefile() as f:
|
|
||||||
while True:
|
while True:
|
||||||
line = f.readline()
|
line = f.readline()
|
||||||
result = json.loads(line)
|
result = json.loads(line)
|
||||||
tag = result.get("tag", None)
|
tag = result.get("tag", None)
|
||||||
|
|
||||||
if tag == "ret":
|
if tag == "ret":
|
||||||
return result.get("val", None), result.get("exception", None)
|
return result.get("val", None), result.get("err", None)
|
||||||
elif tag == "out":
|
elif tag == "out":
|
||||||
print(result.get("val"), end="")
|
print(result.get("val"), end="")
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unexpected response from PREPL")
|
raise RuntimeError("unexpected response from PREPL")
|
||||||
|
|
||||||
def encode(val):
|
def print_error(error):
|
||||||
return json.dumps(json.dumps(val))
|
print("ERR:", error["hint"])
|
||||||
|
|
||||||
def print_error(res):
|
|
||||||
for error in res["via"]:
|
|
||||||
print("ERR:", error["message"])
|
|
||||||
break
|
|
||||||
|
|
||||||
def run_cmd(params):
|
def run_cmd(params):
|
||||||
try:
|
try:
|
||||||
expr = "(app.srepl.cli/exec {})".format(encode(params))
|
res, err = send(params)
|
||||||
res, failed = send_eval(expr)
|
if err:
|
||||||
if failed:
|
print_error(err)
|
||||||
print_error(res)
|
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -96,7 +91,7 @@ def update_profile(email, fullname, password, is_active):
|
||||||
"email": email,
|
"email": email,
|
||||||
"fullname": fullname,
|
"fullname": fullname,
|
||||||
"password": password,
|
"password": password,
|
||||||
"is_active": is_active
|
"isActive": is_active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +133,7 @@ def derive_password(password):
|
||||||
params = {
|
params = {
|
||||||
"cmd": "derive-password",
|
"cmd": "derive-password",
|
||||||
"params": {
|
"params": {
|
||||||
"password": password,
|
"password": password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,9 +76,10 @@
|
||||||
(perms/make-check-fn has-read-permissions?))
|
(perms/make-check-fn has-read-permissions?))
|
||||||
|
|
||||||
(defn decode-row
|
(defn decode-row
|
||||||
[{:keys [features] :as row}]
|
[{:keys [features subscription] :as row}]
|
||||||
(cond-> row
|
(cond-> row
|
||||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
(some? features) (assoc :features (db/decode-pgarray features #{}))
|
||||||
|
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
|
||||||
|
|
||||||
;; FIXME: move
|
;; FIXME: move
|
||||||
|
|
||||||
|
@ -126,16 +127,40 @@
|
||||||
(get-teams conn profile-id)))
|
(get-teams conn profile-id)))
|
||||||
|
|
||||||
(def sql:get-teams-with-permissions
|
(def sql:get-teams-with-permissions
|
||||||
"select t.*,
|
"SELECT t.*,
|
||||||
tp.is_owner,
|
tp.is_owner,
|
||||||
tp.is_admin,
|
tp.is_admin,
|
||||||
tp.can_edit,
|
tp.can_edit,
|
||||||
(t.id = ?) as is_default
|
(t.id = ?) AS is_default
|
||||||
from team_profile_rel as tp
|
FROM team_profile_rel AS tp
|
||||||
join team as t on (t.id = tp.team_id)
|
JOIN team AS t ON (t.id = tp.team_id)
|
||||||
where t.deleted_at is null
|
WHERE t.deleted_at IS null
|
||||||
and tp.profile_id = ?
|
AND tp.profile_id = ?
|
||||||
order by tp.created_at asc")
|
ORDER BY tp.created_at ASC")
|
||||||
|
|
||||||
|
(def sql:get-teams-with-permissions-and-subscription
|
||||||
|
"SELECT t.*,
|
||||||
|
tp.is_owner,
|
||||||
|
tp.is_admin,
|
||||||
|
tp.can_edit,
|
||||||
|
(t.id = ?) AS is_default,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'~:type', COALESCE(p.props->'~:subscription'->>'~:type', 'professional'),
|
||||||
|
'~:status', CASE COALESCE(p.props->'~:subscription'->>'~:type', 'professional')
|
||||||
|
WHEN 'professional' THEN 'active'
|
||||||
|
ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete')
|
||||||
|
END
|
||||||
|
) AS subscription
|
||||||
|
FROM team_profile_rel AS tp
|
||||||
|
JOIN team AS t ON (t.id = tp.team_id)
|
||||||
|
JOIN team_profile_rel AS tpr
|
||||||
|
ON (tpr.team_id = t.id AND tpr.is_owner IS true)
|
||||||
|
JOIN profile AS p
|
||||||
|
ON (tpr.profile_id = p.id)
|
||||||
|
WHERE t.deleted_at IS null
|
||||||
|
AND tp.profile_id = ?
|
||||||
|
ORDER BY tp.created_at ASC;")
|
||||||
|
|
||||||
(defn process-permissions
|
(defn process-permissions
|
||||||
[team]
|
[team]
|
||||||
|
@ -150,13 +175,21 @@
|
||||||
(dissoc :is-owner :is-admin :can-edit)
|
(dissoc :is-owner :is-admin :can-edit)
|
||||||
(assoc :permissions permissions))))
|
(assoc :permissions permissions))))
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
xform:process-teams
|
||||||
|
(comp
|
||||||
|
(map decode-row)
|
||||||
|
(map process-permissions)))
|
||||||
|
|
||||||
(defn get-teams
|
(defn get-teams
|
||||||
[conn profile-id]
|
[conn profile-id]
|
||||||
(let [profile (profile/get-profile conn profile-id)]
|
(let [profile (profile/get-profile conn profile-id)
|
||||||
(->> (db/exec! conn [sql:get-teams-with-permissions (:default-team-id profile) profile-id])
|
sql (if (contains? cf/flags :subscriptions)
|
||||||
(map decode-row)
|
sql:get-teams-with-permissions-and-subscription
|
||||||
(map process-permissions)
|
sql:get-teams-with-permissions)]
|
||||||
(vec))))
|
|
||||||
|
(->> (db/exec! conn [sql (:default-team-id profile) profile-id])
|
||||||
|
(into [] xform:process-teams))))
|
||||||
|
|
||||||
;; --- Query: Team (by ID)
|
;; --- Query: Team (by ID)
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,17 @@
|
||||||
|
|
||||||
(ns app.srepl
|
(ns app.srepl
|
||||||
"Server Repl."
|
"Server Repl."
|
||||||
|
(:refer-clojure :exclude [read-line])
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.json :as json]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.srepl.cli]
|
[app.srepl.cli :as cli]
|
||||||
[app.srepl.main]
|
[app.srepl.main]
|
||||||
[app.util.json :as json]
|
|
||||||
[app.util.locks :as locks]
|
[app.util.locks :as locks]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.core :as c]
|
||||||
[clojure.core.server :as ccs]
|
[clojure.core.server :as ccs]
|
||||||
[clojure.main :as cm]
|
[clojure.main :as cm]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
@ -28,17 +32,80 @@
|
||||||
:init repl-init
|
:init repl-init
|
||||||
:read ccs/repl-read))
|
:read ccs/repl-read))
|
||||||
|
|
||||||
|
(defn- ex->data
|
||||||
|
[cause phase]
|
||||||
|
(let [data (ex-data cause)
|
||||||
|
explain (ex/explain data)]
|
||||||
|
(cond-> {:phase phase
|
||||||
|
:code (get data :code :unknown)
|
||||||
|
:type (get data :type :unknown)
|
||||||
|
:hint (or (get data :hint) (ex-message cause))}
|
||||||
|
(some? explain)
|
||||||
|
(assoc :explain explain))))
|
||||||
|
|
||||||
|
(defn read-line
|
||||||
|
[]
|
||||||
|
(if-let [line (c/read-line)]
|
||||||
|
(try
|
||||||
|
(l/dbg :hint "decode" :data line)
|
||||||
|
(json/decode line :key-fn json/read-kebab-key)
|
||||||
|
(catch Throwable _cause
|
||||||
|
(l/warn :hint "unable to decode data" :data line)
|
||||||
|
nil))
|
||||||
|
::eof))
|
||||||
|
|
||||||
(defn json-repl
|
(defn json-repl
|
||||||
[]
|
[]
|
||||||
(let [out *out*
|
(let [lock (locks/create)
|
||||||
lock (locks/create)]
|
out *out*
|
||||||
(ccs/prepl *in*
|
|
||||||
|
out-fn
|
||||||
(fn [m]
|
(fn [m]
|
||||||
(binding [*out* out,
|
|
||||||
*flush-on-newline* true,
|
|
||||||
*print-readably* true]
|
|
||||||
(locks/locking lock
|
(locks/locking lock
|
||||||
(println (json/encode-str m))))))))
|
(binding [*out* out]
|
||||||
|
(l/warn :hint "write" :data m)
|
||||||
|
(println (json/encode m :key-fn json/write-camel-key)))))
|
||||||
|
|
||||||
|
tapfn
|
||||||
|
(fn [val]
|
||||||
|
(out-fn {:tag :tap :val val}))]
|
||||||
|
|
||||||
|
(binding [*out* (PrintWriter-on #(out-fn {:tag :out :val %1}) nil true)
|
||||||
|
*err* (PrintWriter-on #(out-fn {:tag :err :val %1}) nil true)]
|
||||||
|
(try
|
||||||
|
(add-tap tapfn)
|
||||||
|
(loop []
|
||||||
|
(when (try
|
||||||
|
(let [data (read-line)
|
||||||
|
tpoint (dt/tpoint)]
|
||||||
|
|
||||||
|
(l/dbg :hint "received" :data (if (= data ::eof) "EOF" data))
|
||||||
|
|
||||||
|
(try
|
||||||
|
(when-not (= data ::eof)
|
||||||
|
(when-not (nil? data)
|
||||||
|
(let [result (cli/exec data)
|
||||||
|
elapsed (tpoint)]
|
||||||
|
(l/warn :hint "result" :data result)
|
||||||
|
(out-fn {:tag :ret
|
||||||
|
:val (if (instance? Throwable result)
|
||||||
|
(Throwable->map result)
|
||||||
|
result)
|
||||||
|
:elapsed (inst-ms elapsed)})))
|
||||||
|
true)
|
||||||
|
(catch Throwable cause
|
||||||
|
(let [elapsed (tpoint)]
|
||||||
|
(out-fn {:tag :ret
|
||||||
|
:err (ex->data cause :eval)
|
||||||
|
:elapsed (inst-ms elapsed)})
|
||||||
|
true))))
|
||||||
|
(catch Throwable cause
|
||||||
|
(out-fn {:tag :ret
|
||||||
|
:err (ex->data cause :read)})
|
||||||
|
true))
|
||||||
|
(recur)))
|
||||||
|
(finally
|
||||||
|
(remove-tap tapfn))))))
|
||||||
|
|
||||||
;; --- State initialization
|
;; --- State initialization
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,23 @@
|
||||||
(:require
|
(:require
|
||||||
[app.auth :as auth]
|
[app.auth :as auth]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.schema :as sm]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.rpc.commands.profile :as cmd.profile]
|
[app.rpc.commands.profile :as cmd.profile]
|
||||||
[app.util.json :as json]
|
[app.setup :as-alias setup]
|
||||||
|
[app.tokens :as tokens]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(defn coercer
|
||||||
|
[schema & {:as opts}]
|
||||||
|
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||||
|
check-fn (sm/check-fn schema opts)]
|
||||||
|
(fn [data]
|
||||||
|
(-> data decode-fn check-fn))))
|
||||||
|
|
||||||
(defn- get-current-system
|
(defn- get-current-system
|
||||||
[]
|
[]
|
||||||
(or (deref (requiring-resolve 'app.main/system))
|
(or (deref (requiring-resolve 'app.main/system))
|
||||||
|
@ -24,16 +33,21 @@
|
||||||
|
|
||||||
(defmulti ^:private exec-command ::cmd)
|
(defmulti ^:private exec-command ::cmd)
|
||||||
|
|
||||||
|
(defmethod exec-command :default
|
||||||
|
[{:keys [::cmd]}]
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :not-implemented
|
||||||
|
:hint (str/ffmt "command '%' not implemented" cmd)))
|
||||||
|
|
||||||
(defn exec
|
(defn exec
|
||||||
"Entry point with external tools integrations that uses PREPL
|
"Entry point with external tools integrations that uses PREPL
|
||||||
interface for interacting with running penpot backend."
|
interface for interacting with running penpot backend."
|
||||||
[data]
|
[data]
|
||||||
(let [data (json/decode data)]
|
(-> {::cmd (get data :cmd)}
|
||||||
(-> {::cmd (keyword (:cmd data "default"))}
|
|
||||||
(merge (:params data))
|
(merge (:params data))
|
||||||
(exec-command))))
|
(exec-command)))
|
||||||
|
|
||||||
(defmethod exec-command :create-profile
|
(defmethod exec-command "create-profile"
|
||||||
[{:keys [fullname email password is-active]
|
[{:keys [fullname email password is-active]
|
||||||
:or {is-active true}}]
|
:or {is-active true}}]
|
||||||
(some-> (get-current-system)
|
(some-> (get-current-system)
|
||||||
|
@ -49,7 +63,7 @@
|
||||||
(->> (cmd.auth/create-profile! conn params)
|
(->> (cmd.auth/create-profile! conn params)
|
||||||
(cmd.auth/create-profile-rels! conn)))))))
|
(cmd.auth/create-profile-rels! conn)))))))
|
||||||
|
|
||||||
(defmethod exec-command :update-profile
|
(defmethod exec-command "update-profile"
|
||||||
[{:keys [fullname email password is-active]}]
|
[{:keys [fullname email password is-active]}]
|
||||||
(some-> (get-current-system)
|
(some-> (get-current-system)
|
||||||
(db/tx-run!
|
(db/tx-run!
|
||||||
|
@ -70,7 +84,12 @@
|
||||||
:deleted-at nil})]
|
:deleted-at nil})]
|
||||||
(pos? (db/get-update-count res)))))))))
|
(pos? (db/get-update-count res)))))))))
|
||||||
|
|
||||||
(defmethod exec-command :delete-profile
|
(defmethod exec-command "echo"
|
||||||
|
[params]
|
||||||
|
params)
|
||||||
|
|
||||||
|
|
||||||
|
(defmethod exec-command "delete-profile"
|
||||||
[{:keys [email soft]}]
|
[{:keys [email soft]}]
|
||||||
(when-not email
|
(when-not email
|
||||||
(ex/raise :type :assertion
|
(ex/raise :type :assertion
|
||||||
|
@ -88,7 +107,7 @@
|
||||||
{:email email}))]
|
{:email email}))]
|
||||||
(pos? (db/get-update-count res)))))))
|
(pos? (db/get-update-count res)))))))
|
||||||
|
|
||||||
(defmethod exec-command :search-profile
|
(defmethod exec-command "search-profile"
|
||||||
[{:keys [email]}]
|
[{:keys [email]}]
|
||||||
(when-not email
|
(when-not email
|
||||||
(ex/raise :type :assertion
|
(ex/raise :type :assertion
|
||||||
|
@ -102,12 +121,130 @@
|
||||||
" where email similar to ? order by created_at desc limit 100")]
|
" where email similar to ? order by created_at desc limit 100")]
|
||||||
(db/exec! conn [sql email]))))))
|
(db/exec! conn [sql email]))))))
|
||||||
|
|
||||||
(defmethod exec-command :derive-password
|
(defmethod exec-command "derive-password"
|
||||||
[{:keys [password]}]
|
[{:keys [password]}]
|
||||||
(auth/derive-password password))
|
(auth/derive-password password))
|
||||||
|
|
||||||
(defmethod exec-command :default
|
(defmethod exec-command "authenticate"
|
||||||
[{:keys [::cmd]}]
|
[{:keys [token]}]
|
||||||
(ex/raise :type :internal
|
(when-let [system (get-current-system)]
|
||||||
:code :not-implemented
|
(let [props (get system ::setup/props)]
|
||||||
:hint (str/ffmt "command '%' not implemented" (name cmd))))
|
(tokens/verify props {:token token :iss "authentication"}))))
|
||||||
|
|
||||||
|
(def ^:private schema:get-customer
|
||||||
|
[:map [:id ::sm/uuid]])
|
||||||
|
|
||||||
|
(def coerce-get-customer-params
|
||||||
|
(coercer schema:get-customer
|
||||||
|
:type :validation
|
||||||
|
:hint "invalid data provided for `get-customer` rpc call"))
|
||||||
|
|
||||||
|
(def sql:get-customer-slots
|
||||||
|
"WITH teams AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
tpr.profile_id AS profile_id
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.is_owner IS true
|
||||||
|
AND tpr.profile_id = ?
|
||||||
|
), teams_with_slots AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
count(*) AS total
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||||
|
AND tpr.can_edit IS true
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 2
|
||||||
|
)
|
||||||
|
SELECT max(total) AS total FROM teams_with_slots;")
|
||||||
|
|
||||||
|
(defn- get-customer-slots
|
||||||
|
[system profile-id]
|
||||||
|
(let [result (db/exec-one! system [sql:get-customer-slots profile-id])]
|
||||||
|
(:total result)))
|
||||||
|
|
||||||
|
(defmethod exec-command "get-customer"
|
||||||
|
[params]
|
||||||
|
(when-let [system (get-current-system)]
|
||||||
|
(let [{:keys [id] :as params} (coerce-get-customer-params params)
|
||||||
|
{:keys [props] :as profile} (cmd.profile/get-profile system id)]
|
||||||
|
{:id (get profile :id)
|
||||||
|
:name (get profile :fullname)
|
||||||
|
:email (get profile :email)
|
||||||
|
:num-editors (get-customer-slots system id)
|
||||||
|
:subscription (get props :subscription)})))
|
||||||
|
|
||||||
|
(def ^:private schema:customer-subscription
|
||||||
|
[:map {:title "CustomerSubscription"}
|
||||||
|
[:id ::sm/text]
|
||||||
|
[:customer-id ::sm/text]
|
||||||
|
[:type [:enum
|
||||||
|
"unlimited"
|
||||||
|
"professional"
|
||||||
|
"enterprise"]]
|
||||||
|
[:status [:enum
|
||||||
|
"active"
|
||||||
|
"canceled"
|
||||||
|
"incomplete"
|
||||||
|
"incomplete_expired"
|
||||||
|
"pass_due"
|
||||||
|
"paused"
|
||||||
|
"trialing"
|
||||||
|
"unpaid"]]
|
||||||
|
|
||||||
|
[:billing-period [:enum
|
||||||
|
"month"
|
||||||
|
"day"
|
||||||
|
"week"
|
||||||
|
"year"]]
|
||||||
|
[:quantity :int]
|
||||||
|
[:description [:maybe ::sm/text]]
|
||||||
|
[:created-at ::sm/timestamp]
|
||||||
|
[:start-date [:maybe ::sm/timestamp]]
|
||||||
|
[:ended-at [:maybe ::sm/timestamp]]
|
||||||
|
[:trial-end [:maybe ::sm/timestamp]]
|
||||||
|
[:trial-start [:maybe ::sm/timestamp]]
|
||||||
|
[:cancel-at [:maybe ::sm/timestamp]]
|
||||||
|
[:canceled-at [:maybe ::sm/timestamp]]
|
||||||
|
|
||||||
|
[:current-period-end ::sm/timestamp]
|
||||||
|
[:current-period-start ::sm/timestamp]
|
||||||
|
[:cancel-at-period-end :boolean]
|
||||||
|
|
||||||
|
[:cancellation-details
|
||||||
|
[:map {:title "CancellationDetails"}
|
||||||
|
[:comment [:maybe ::sm/text]]
|
||||||
|
[:reason [:maybe ::sm/text]]
|
||||||
|
[:feedback [:maybe
|
||||||
|
[:enum
|
||||||
|
"customer_service"
|
||||||
|
"low_quality"
|
||||||
|
"missing_feature"
|
||||||
|
"other"
|
||||||
|
"switched_service"
|
||||||
|
"too_complex"
|
||||||
|
"too_expensive"
|
||||||
|
"unused"]]]]]])
|
||||||
|
|
||||||
|
(def ^:private schema:update-customer-subscription
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:subscription [:maybe schema:customer-subscription]]])
|
||||||
|
|
||||||
|
(def coerce-update-customer-subscription-params
|
||||||
|
(coercer schema:update-customer-subscription
|
||||||
|
:type :validation
|
||||||
|
:hint "invalid data provided for `update-customer-subscription` rpc call"))
|
||||||
|
|
||||||
|
(defmethod exec-command "update-customer-subscription"
|
||||||
|
[params]
|
||||||
|
(when-let [system (get-current-system)]
|
||||||
|
(let [{:keys [id subscription]} (coerce-update-customer-subscription-params params)
|
||||||
|
;; FIXME: locking
|
||||||
|
{:keys [props] :as profile} (cmd.profile/get-profile system id)
|
||||||
|
props (assoc props :subscription subscription)]
|
||||||
|
|
||||||
|
(db/update! system :profile
|
||||||
|
{:props (db/tjson props)}
|
||||||
|
{:id id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
true)))
|
||||||
|
|
|
@ -907,6 +907,22 @@
|
||||||
::oapi/type "string"
|
::oapi/type "string"
|
||||||
::oapi/format "iso"}})
|
::oapi/format "iso"}})
|
||||||
|
|
||||||
|
(register!
|
||||||
|
{:type ::timestamp
|
||||||
|
:pred inst?
|
||||||
|
:type-properties
|
||||||
|
{:title "inst"
|
||||||
|
:description "Satisfies Inst protocol"
|
||||||
|
:error/message "should be an instant"
|
||||||
|
:gen/gen (->> (sg/small-int)
|
||||||
|
(sg/fmap (fn [v] (tm/parse-instant v))))
|
||||||
|
:decode/string tm/parse-instant
|
||||||
|
:encode/string inst-ms
|
||||||
|
:decode/json tm/parse-instant
|
||||||
|
:encode/json inst-ms
|
||||||
|
::oapi/type "string"
|
||||||
|
::oapi/format "number"}})
|
||||||
|
|
||||||
(register!
|
(register!
|
||||||
{:type ::fn
|
{:type ::fn
|
||||||
:pred fn?})
|
:pred fn?})
|
||||||
|
|
Loading…
Add table
Reference in a new issue