penpot/backend/src/app/rpc/quotes.clj
Andrey Antukh 6fa440cf92 🎉 Add chunked upload API for large media and binary files
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.

Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
  created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
    * `create-upload-session`  – allocates a session row; enforces
      `upload-sessions-per-profile` and `upload-chunks-per-session`
      quota limits (configurable in config.clj, defaults 5 / 20).
    * `upload-chunk`           – stores each slice as a storage object;
      validates chunk index bounds and profile ownership.
    * `assemble-file-media-object` – reassembles chunks via the shared
      `assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
  `assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
  when provided, materialises the temp file from chunks instead of
  expecting an inline multipart body, removing the 200 MiB body limit
  on .penpot imports.  Schema updated with an `:and` validator requiring
  either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
  (never-completed) sessions older than 1 hour; scheduled daily at
  midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.

Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
  helper drives steps 1–2 (create session + upload all chunks with a
  concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
  via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
  (`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
  use the existing direct `upload-file-media-object` path.
  `handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
  inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
  `form-data?` removed from `import-binfile` (JSON params only).

Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
  isolation, invalid media type, missing chunks, session-not-found,
  chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
  correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
  blobs, chunked path for large blobs, and chunk-count correctness for
  `process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
  `make-json-response`, `make-transit-response`, `url->cmd`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-16 19:43:57 +02:00

559 lines
20 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.quotes
"Penpot resource usage quotes."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.config :as cf]
[app.db :as db]
[app.worker :as wrk]
[cuerdas.core :as str]))
(defmulti check-quote ::id)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:quote
[:map {:title "Quote"}
[::team-id {:optional true} ::sm/uuid]
[::project-id {:optional true} ::sm/uuid]
[::file-id {:optional true} ::sm/uuid]
[::incr {:optional true} [::sm/int {:min 0}]]
[::id :keyword]
[::profile-id ::sm/uuid]])
(def valid-quote?
(sm/lazy-validator schema:quote))
(def ^:private enabled (volatile! true))
(defn enable!
"Enable quotes checking at runtime (from server REPL)."
[]
(vswap! enabled (constantly true)))
(defn disable!
"Disable quotes checking at runtime (from server REPL)."
[]
(vswap! enabled (constantly false)))
(defn- check
[cfg quote]
(let [quote (merge cfg quote)
id (::id quote)]
(when-not (valid-quote? quote)
(ex/raise :type :internal
:code :invalid-quote-definition
:hint "found invalid data for quote schema"
:quote (name id)))
(-> (assoc quote ::target (name id))
(check-quote))))
(defn check!
([cfg]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg check {}))))
([cfg & others]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg (fn [cfg]
(run! (partial check cfg) others)))))))
(defn- send-notification!
[{:keys [::db/conn] :as params}]
(l/warn :hint "max quote reached"
:target (::target params)
:profile-id (some-> params ::profile-id str)
:team-id (some-> params ::team-id str)
:project-id (some-> params ::project-id str)
:file-id (some-> params ::file-id str)
:quote (::quote params)
:total (::total params)
:incr (::inc params 1))
(when-let [admins (seq (cf/get :admins))]
(let [subject (str/istr "[quotes:notification]: max quote reached ~(::target params)")
content (str/istr "- Param: profile-id '~(::profile-id params)}'\n"
"- Param: team-id '~(::team-id params)'\n"
"- Param: project-id '~(::project-id params)'\n"
"- Param: file-id '~(::file-id params)'\n"
"- Quote ID: '~(::target params)'\n"
"- Max: ~(::quote params)\n"
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
(wrk/submit! {::db/conn conn
::wrk/task :sendmail
::wrk/delay (ct/duration "30s")
::wrk/max-retries 4
::wrk/priority 200
::wrk/dedupe true
::wrk/label "quotes-notification"
::wrk/params {:to (vec admins)
:subject subject
:body content}}))))
(defn- generic-check!
[{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
(let [quote (->> (db/exec! conn quote-sql)
(map :quote)
(reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default)
total (:total (db/exec-one! conn count-sql))]
(when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes)
(send-notification! (assoc params ::quote quote ::total total))
(ex/raise :type :restriction
:code :max-quote-reached
:target target
:quote quote
:count total)))))
(def ^:private sql:get-quotes-1
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND profile_id = ?
AND team_id IS NULL
AND project_id IS NULL
AND file_id IS NULL;")
(def ^:private sql:get-quotes-2
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-3
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-4
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:teams-per-profile
[:map [::profile-id ::sm/uuid]])
(def ^:private valid-teams-per-profile-quote?
(sm/lazy-validator schema:teams-per-profile))
(def ^:private sql:get-teams-per-profile
"SELECT count(*) AS total
FROM team_profile_rel
WHERE profile_id = ?")
(defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}]
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:access-tokens-per-profile
[:map [::profile-id ::sm/uuid]])
(def ^:private valid-access-tokens-per-profile-quote?
(sm/lazy-validator schema:access-tokens-per-profile))
(def ^:private sql:get-access-tokens-per-profile
"SELECT count(*) AS total
FROM access_token
WHERE profile_id = ?")
(defmethod check-quote ::access-tokens-per-profile
[{:keys [::profile-id ::target] :as quote}]
(assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-access-tokens-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:projects-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-projects-per-team-quote?
(sm/lazy-validator schema:projects-per-team))
(def ^:private sql:get-projects-per-team
"SELECT count(*) AS total
FROM project AS p
WHERE p.team_id = ?
AND p.deleted_at IS NULL")
(defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:font-variants-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-font-variant-per-team-quote?
(sm/lazy-validator schema:font-variants-per-team))
(def ^:private sql:get-font-variants-per-team
"SELECT count(*) AS total
FROM team_font_variant AS v
WHERE v.team_id = ?")
(defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-font-variants-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:invitations-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-invitations-per-team-quote?
(sm/lazy-validator schema:invitations-per-team))
(def ^:private sql:get-invitations-per-team
"SELECT count(*) AS total
FROM team_invitation
WHERE team_id = ?")
(defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:profiles-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-profiles-per-team-quote?
(sm/lazy-validator schema:profiles-per-team))
(def ^:private sql:get-profiles-per-team
"SELECT (SELECT count(*)
FROM team_profile_rel
WHERE team_id = ?) +
(SELECT count(*)
FROM team_invitation
WHERE team_id = ?
AND valid_until > now()) AS total;")
;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations.
(defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:files-per-project
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-files-per-project-quote?
(sm/lazy-validator schema:files-per-project))
(def ^:private sql:get-files-per-project
"SELECT count(*) AS total
FROM file AS f
WHERE f.project_id = ?
AND f.deleted_at IS NULL")
(defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
(assert (valid-files-per-project-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-files-per-project project-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:comment-threads-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-comment-threads-per-file-quote?
(sm/lazy-validator schema:comment-threads-per-file))
(def ^:private sql:get-comment-threads-per-file
"SELECT count(*) AS total
FROM comment_thread AS ct
WHERE ct.file_id = ?")
(defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:comments-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-comments-per-file-quote?
(sm/lazy-validator schema:comments-per-file))
(def ^:private sql:get-comments-per-file
"SELECT count(*) AS total
FROM comment AS c
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
WHERE ct.file_id = ?")
(defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-comments-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: SNAPSHOTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:snapshots-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]
[::file-id ::sm/uuid]])
(def ^:private valid-snapshots-per-file-quote?
(sm/lazy-validator schema:snapshots-per-file))
(def ^:private sql:get-snapshots-per-file
"SELECT count(*) AS total
FROM file_change AS fc
WHERE fc.file_id = ?
AND fc.created_by = 'user'
AND fc.deleted_at IS NULL
AND fc.data IS NOT NULL")
(defmethod check-quote ::snapshots-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(assert (valid-snapshots-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-snapshots-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-snapshots-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: SNAPSHOTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:snapshots-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-snapshots-per-team-quote?
(sm/lazy-validator schema:snapshots-per-team))
(def ^:private sql:get-snapshots-per-team
"SELECT count(*) AS total
FROM file_change AS fc
JOIN file AS f ON (f.id = fc.file_id)
JOIN project AS p ON (p.id = f.project_id)
WHERE p.team_id = ?
AND fc.created_by = 'user'
AND fc.deleted_at IS NULL
AND fc.data IS NOT NULL")
(defmethod check-quote ::snapshots-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-snapshots-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-snapshots-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-snapshots-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAM-ACCESS-REQUESTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:team-access-requests-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-team-access-requests-per-team-quote?
(sm/lazy-validator schema:team-access-requests-per-team))
(def ^:private sql:get-team-access-requests-per-team
"SELECT count(*) AS total
FROM team_access_request AS tar
WHERE tar.team_id = ?")
(defmethod check-quote ::team-access-requests-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-team-access-requests-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-team-access-requests-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-team-access-requests-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAM-ACCESS-REQUESTS-PER-REQUESTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:team-access-requests-per-requester
[:map
[::profile-id ::sm/uuid]])
(def ^:private valid-team-access-requests-per-requester-quote?
(sm/lazy-validator schema:team-access-requests-per-requester))
(def ^:private sql:get-team-access-requests-per-requester
"SELECT count(*) AS total
FROM team_access_request AS tar
WHERE tar.requester_id = ?")
(defmethod check-quote ::team-access-requests-per-requester
[{:keys [::profile-id ::target] :as quote}]
(assert (valid-team-access-requests-per-requester-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-team-access-requests-per-requester Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-team-access-requests-per-requester profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: UPLOAD-SESSIONS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:upload-sessions-per-profile
[:map [::profile-id ::sm/uuid]])
(def ^:private valid-upload-sessions-per-profile-quote?
(sm/lazy-validator schema:upload-sessions-per-profile))
(def ^:private sql:get-upload-sessions-per-profile
"SELECT count(*) AS total
FROM upload_session
WHERE profile_id = ?")
(defmethod check-quote ::upload-sessions-per-profile
[{:keys [::profile-id ::target] :as quote}]
(assert (valid-upload-sessions-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-upload-sessions-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-upload-sessions-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: DEFAULT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod check-quote :default
[{:keys [::id]}]
(ex/raise :type :internal
:code :quote-not-defined
:quote id
:hint "backend using a quote identifier not defined"))