diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 7f4b470ab3..3fc4fefe68 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -259,6 +259,14 @@ (generate-nitrate-uri "api/teams/" team-id) cto/schema:team-with-organization params)) +(defn- get-teams-organizations-api + [cfg {:keys [team-ids] :as params}] + (let [params (assoc params :request-params {:team-ids team-ids})] + (request-to-nitrate cfg :post + (generate-nitrate-uri "api/teams/organizations") + [:vector cto/schema:team-with-organization] + params))) + (defn- get-organization-membership-api [cfg {:keys [profile-id organization-id] :as params}] (request-to-nitrate cfg :get @@ -489,6 +497,7 @@ [_ cfg] (when (contains? cf/flags :admin-console) {:get-team-organization (partial get-team-organization-api cfg) + :get-teams-organizations (partial get-teams-organizations-api cfg) :set-team-organization (partial set-team-organization-api cfg) :get-organization-membership (partial get-organization-membership-api cfg) :get-organization-membership-by-team (partial get-organization-membership-by-team-api cfg) @@ -596,22 +605,25 @@ :cause cause) profile))))) +(defn- apply-organization-info-to-team + [team team-with-organization] + (let [organization (:organization team-with-organization)] + (if (some? organization) + (-> (cto/apply-organization team (assoc organization :custom-photo + (when-let [logo-id (:logo-id organization)] + (generate-public-uri "assets/by-id/" logo-id)))) + (assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-organization))))) + team))) + (defn add-organization-info-to-team "Enriches a team map with organization information from Nitrate. - Adds organization-id, organization-name, organization-slug, organization-owner-id, and your-penpot fields. Returns the original team unchanged if the request fails or organization data is nil. Propagates `:nitrate-unavailable` so the request is rejected when Nitrate is unreachable." [cfg team params] (try - (let [params (assoc (or params {}) :team-id (:id team)) - team-with-organization (call cfg :get-team-organization params) - organization (:organization team-with-organization)] - (if (some? organization) - (-> (cto/apply-organization team (assoc organization :custom-photo - (when-let [logo-id (:logo-id organization)] - (generate-public-uri "assets/by-id/" logo-id)))) - (assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-organization))))) - team)) + (let [params (assoc (or params {}) :team-id (:id team)) + team-with-organization (call cfg :get-team-organization params)] + (apply-organization-info-to-team team team-with-organization)) (catch Throwable cause (if (= :nitrate-unavailable (-> cause ex-data :type)) (throw cause) @@ -621,6 +633,23 @@ :cause cause) team))))) +(defn add-organization-info-to-teams + "Enriches teams with organization information using one batched Nitrate request. + Teams absent from the Nitrate response are returned unchanged. + Rejects the request when Nitrate does not return a valid batch response." + [cfg teams params] + (let [request-params (assoc (or params {}) :team-ids (mapv :id teams)) + teams-with-organization (call cfg :get-teams-organizations request-params)] + (when (nil? teams-with-organization) + (ex/raise :type :nitrate-unavailable + :hint "nitrate did not return a valid teams organization response")) + (let [organizations-by-team (into {} (map (juxt :id identity)) teams-with-organization)] + (mapv (fn [{:keys [id] :as team}] + (if-let [team-with-organization (get organizations-by-team id)] + (apply-organization-info-to-team team team-with-organization) + team)) + teams)))) + (defn set-team-organization "Associates a team with an organization in Nitrate. Requires organization-id and is-default in params. diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 196b35c051..0467082b1a 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -196,11 +196,11 @@ ::sm/params schema:get-teams} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (dm/with-open [conn (db/open pool)] - (cond->> (get-teams conn profile-id) - (contains? cf/flags :admin-console) - (map #(nitrate/add-organization-info-to-team cfg % params)) - (contains? cf/flags :admin-console) - (remove #(get-in % [:organization :expired-license]))))) + (let [teams (get-teams conn profile-id)] + (if (contains? cf/flags :admin-console) + (->> (nitrate/add-organization-info-to-teams cfg teams params) + (remove #(get-in % [:organization :expired-license]))) + teams)))) (def ^:private sql:get-owned-teams "SELECT t.id, t.name, diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 5c0477ed17..920defb2ba 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -719,6 +719,67 @@ (t/is (not= (:default-team-id profile1) (:id item1)))))) +(t/deftest get-teams-fetches-organizations-in-one-batch + (let [profile (th/create-profile* 1 {:is-active true}) + organization-team (th/create-team* 1 {:profile-id (:id profile)}) + plain-team (th/create-team* 2 {:profile-id (:id profile)}) + expired-team (th/create-team* 3 {:profile-id (:id profile)}) + organization-id (uuid/random) + calls (atom []) + organization {:id organization-id + :name "Acme" + :slug "acme" + :owner-id (:id profile) + :avatar-bg-url "https://example.com/avatar.svg"} + nitrate-call (fn [_cfg method params] + (swap! calls conj [method params]) + [{:id (:id organization-team) + :is-your-penpot false + :organization organization} + {:id (:id expired-team) + :is-your-penpot false + :organization (assoc organization :expired-license true)}]) + params {::th/type :get-teams + ::rpc/profile-id (:id profile)}] + (with-redefs [cf/flags (conj cf/flags :admin-console) + nitrate/call nitrate-call] + (let [out (th/command! params) + teams (:result out)] + (t/is (th/success? out)) + (t/is (= 1 (count @calls))) + (t/is (= :get-teams-organizations (ffirst @calls))) + (t/is (= #{(:default-team-id profile) + (:id organization-team) + (:id plain-team) + (:id expired-team)} + (-> @calls first second :team-ids set))) + (t/is (= #{(:default-team-id profile) + (:id organization-team) + (:id plain-team)} + (into #{} (map :id) teams))) + (t/is (= organization + (->> teams + (filter #(= (:id organization-team) (:id %))) + first + :organization))))))) + + +(t/deftest get-teams-rejects-invalid-organization-batch-response + (let [profile (th/create-profile* 1 {:is-active true}) + calls (atom []) + params {::th/type :get-teams + ::rpc/profile-id (:id profile)}] + (with-redefs [cf/flags (conj cf/flags :admin-console) + nitrate/call (fn [_cfg method call-params] + (swap! calls conj [method call-params]) + nil)] + (let [out (th/command! params)] + (t/is (not (th/success? out))) + (t/is (= :nitrate-unavailable (th/ex-type (:error out)))) + (t/is (= 1 (count @calls))) + (t/is (= :get-teams-organizations (ffirst @calls))))))) + + (t/deftest team-deletion-1 (let [profile1 (th/create-profile* 1 {:is-active true}) team (th/create-team* 1 {:profile-id (:id profile1)})