mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 10:26:14 +00:00
* ✨ Add upload_session_chunk indirection for chunked uploads Chunks now live in the upload_session_chunk table with non-deleting foreign keys to storage_object and upload_session, instead of tempfile objects with session metadata. Reads go through a JOIN, so chunk state never scans storage_object. Uploads validate the live session, reject duplicate indexes, and store objects in the new upload-session bucket without extra metadata. Assemble removes mappings and marks the session consumed; objects-gc procedurally purges consumed and stalled sessions, touching referenced objects first. Touched-gc and deleted-gc handle the new bucket, and upload-session-gc is removed. Closes #11644 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Fix quota, give-up and coverage for session chunks Exclude consumed sessions from the sessions-per-profile quota so finished uploads free their slot at once. Remove chunk mappings before the gc-deleted give-up delete to respect the NO ACTION keys. Catch java.sql.SQLException for duplicate chunks. Cover the profile-owned session purge and the UNIQUE race backstop with tests. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Align chunked upload tests with upload_session_chunk Drop the duplicate-index tests written against metadata-backed chunks; the UNIQUE mapping makes those cases unrepresentable and the new tests cover them. Rewrite the rejected-duplicate tests to expect :validation/:chunk-already-exists and assert against the upload_session_chunk table, and scope the chunk-too-large "nothing stored" check to the mapping table. AI-assisted-by: muse-spark-1.3-contributor * ♻️ Use NO ACTION DEFERRABLE session FKs in single migration Fold the profile FK change into 0154 so the feature ships one migration. All three upload session FKs use ON DELETE NO ACTION DEFERRABLE: identical to RESTRICT in normal operation, but deferrable for tooling that relies on SET CONSTRAINTS ALL DEFERRED. Extend the RESTRICT test to the direct profile delete. AI-assisted-by: muse-spark-1.3-contributor * ♻️ Reserve chunk slot before writing blob in upload-chunk Make object_id nullable and insert the mapping with NULL inside the session-locking transaction, then write the blob outside it and link it with a conditional update. A failed write removes the mapping and reraises; a mid-flight death leaves a NULL row and the client starts a new session. AI-assisted-by: muse-spark-1.3-contributor * 🔥 Remove redundant session_id index on upload_session_chunk The UNIQUE(session_id, chunk_index) btree already serves session_id-only lookups and the session FK check through its leftmost column, so the standalone index only taxed the per-chunk INSERT path. Verified with EXPLAIN on an equivalent table shape. AI-assisted-by: muse-spark-1.3-contributor * ⚡ Merge chunk touch and delete into single RETURNING query Replace the SELECT-then-DELETE round-trip in delete-upload-sessions! with DELETE ... RETURNING object_id, touching each returned object. Same semantics, one less query per purged session. Follows the RETURNING pattern already used in file-gc. AI-assisted-by: muse-spark-1.3-contributor * ♻️ Let objects-gc own chunk mapping deletion Assemble-chunks now only marks the session as consumed; the chunk mappings stay until objects-gc purges them (touching the chunk objects first), leaving a single procedural deletion path for consumed, stalled and profile-purge sessions. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Fix font-deletion GC expectations for chunk objects Update final storage-gc-touched counts to include the two chunk objects touched by objects-gc when purging consumed upload sessions (8/5/5 instead of 6/3/3). AI-assisted-by: muse-spark-1.3-contributor * 🐛 Release chunk reservation when the link UPDATE fails Review feedback on #11651: the link UPDATE in upload-chunk could leave a NULL reservation behind, blocking retries of the same index with :chunk-already-exists. Remove the reservation when the link fails so the client can retry in the same session; the orphaned blob stays touched for touched-gc. Also realign the process-bucket! cond branches in gc-touched. Tests: chunked-upload-link-failure-releases-slot and chunked-upload-null-reservation-blocks-retry. AI-assisted-by: muse-spark-1.3-contributor
630 lines
23 KiB
Clojure
630 lines
23 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 SUBSIDIARY SL
|
|
|
|
(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 = ?
|
|
AND deleted_at IS NULL")
|
|
|
|
(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: MEDIA-STORAGE-BYTES-PER-TEAM
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^:private schema:media-storage-bytes-per-team
|
|
[:map
|
|
[::profile-id ::sm/uuid]
|
|
[::team-id ::sm/uuid]])
|
|
|
|
(def ^:private valid-media-storage-bytes-per-team-quote?
|
|
(sm/lazy-validator schema:media-storage-bytes-per-team))
|
|
|
|
(def ^:private sql:get-media-storage-bytes-per-team
|
|
"SELECT COALESCE(SUM(so.size), 0) AS total
|
|
FROM (
|
|
SELECT fmo.media_id AS so_id
|
|
FROM file_media_object AS fmo
|
|
JOIN file AS f ON (f.id = fmo.file_id)
|
|
JOIN project AS p ON (p.id = f.project_id)
|
|
WHERE p.team_id = ?
|
|
AND fmo.deleted_at IS NULL
|
|
AND f.deleted_at IS NULL
|
|
UNION
|
|
SELECT fmo.thumbnail_id AS so_id
|
|
FROM file_media_object AS fmo
|
|
JOIN file AS f ON (f.id = fmo.file_id)
|
|
JOIN project AS p ON (p.id = f.project_id)
|
|
WHERE p.team_id = ?
|
|
AND fmo.thumbnail_id IS NOT NULL
|
|
AND fmo.deleted_at IS NULL
|
|
AND f.deleted_at IS NULL
|
|
UNION
|
|
SELECT v.otf_file_id AS so_id
|
|
FROM team_font_variant AS v
|
|
WHERE v.team_id = ?
|
|
AND v.otf_file_id IS NOT NULL
|
|
AND v.deleted_at IS NULL
|
|
UNION
|
|
SELECT v.ttf_file_id AS so_id
|
|
FROM team_font_variant AS v
|
|
WHERE v.team_id = ?
|
|
AND v.ttf_file_id IS NOT NULL
|
|
AND v.deleted_at IS NULL
|
|
UNION
|
|
SELECT v.woff1_file_id AS so_id
|
|
FROM team_font_variant AS v
|
|
WHERE v.team_id = ?
|
|
AND v.woff1_file_id IS NOT NULL
|
|
AND v.deleted_at IS NULL
|
|
UNION
|
|
SELECT v.woff2_file_id AS so_id
|
|
FROM team_font_variant AS v
|
|
WHERE v.team_id = ?
|
|
AND v.woff2_file_id IS NOT NULL
|
|
AND v.deleted_at IS NULL
|
|
) AS refs
|
|
JOIN storage_object AS so ON (so.id = refs.so_id)
|
|
WHERE so.deleted_at IS NULL")
|
|
|
|
(defmethod check-quote ::media-storage-bytes-per-team
|
|
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
|
(assert (valid-media-storage-bytes-per-team-quote? quote) "invalid quote parameters")
|
|
(-> quote
|
|
(assoc ::default (cf/get :quotes-media-storage-bytes-per-team
|
|
(* 20 1024 1024 1024)))
|
|
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
|
(assoc ::count-sql [sql:get-media-storage-bytes-per-team
|
|
team-id team-id team-id team-id team-id team-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"))
|