From 38d5234a9cfdb0f1ea8a35c33aab3c21010e2709 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 15 Jul 2024 18:37:02 +0900 Subject: [PATCH 01/21] add complexity analysis --- src/com/walmartlabs/lacinia.clj | 26 ++++++--- .../lacinia/complexity_analysis.clj | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/com/walmartlabs/lacinia/complexity_analysis.clj diff --git a/src/com/walmartlabs/lacinia.clj b/src/com/walmartlabs/lacinia.clj index 972899d4..9cdf23b7 100644 --- a/src/com/walmartlabs/lacinia.clj +++ b/src/com/walmartlabs/lacinia.clj @@ -20,7 +20,8 @@ [com.walmartlabs.lacinia.internal-utils :refer [cond-let]] [com.walmartlabs.lacinia.util :refer [as-error-map]] [com.walmartlabs.lacinia.resolve :as resolve] - [com.walmartlabs.lacinia.tracing :as tracing]) + [com.walmartlabs.lacinia.tracing :as tracing] + [com.walmartlabs.lacinia.complexity-analysis :as complexity-analysis]) (:import (clojure.lang ExceptionInfo))) (defn ^:private as-errors @@ -33,11 +34,13 @@ Returns a [[ResolverResult]] that will deliver the result map, or an exception." {:added "0.16.0"} - [parsed-query variables context] - {:pre [(map? parsed-query) - (or (nil? context) - (map? context))]} - (cond-let + ([parsed-query variables context] + (execute-parsed-query-async parsed-query variables context nil)) + ([parsed-query variables context options] + {:pre [(map? parsed-query) + (or (nil? context) + (map? context))]} + (cond-let :let [{:keys [::tracing/timing-start]} parsed-query ;; Validation phase encompasses preparing with query variables and actual validation. ;; It's somewhat all mixed together. @@ -56,10 +59,15 @@ (seq validation-errors) (resolve/resolve-as {:errors validation-errors}) + :let [complexity-error (complexity-analysis/complexity-analysis prepared options)] + + (some? complexity-error) + (resolve/resolve-as {:errors complexity-error}) + :else (executor/execute-query (assoc context constants/parsed-query-key prepared - ::tracing/validation {:start-offset start-offset - :duration (tracing/duration start-nanos)})))) + ::tracing/validation {:start-offset start-offset + :duration (tracing/duration start-nanos)}))))) (defn execute-parsed-query "Prepares a query, by applying query variables to it, resulting in a prepared @@ -76,7 +84,7 @@ {:keys [timeout-ms timeout-error] :or {timeout-ms 0 timeout-error {:message "Query execution timed out."}}} options - execution-result (execute-parsed-query-async parsed-query variables context) + execution-result (execute-parsed-query-async parsed-query variables context options) result (do (resolve/on-deliver! execution-result *result) ;; Block on that deliver, then return the final result. diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj new file mode 100644 index 00000000..096fad99 --- /dev/null +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -0,0 +1,57 @@ +(ns com.walmartlabs.lacinia.complexity-analysis + (:require [clojure.core.match :refer [match]])) + +(defn- parse-query + "- leaf node -> nil + - field & selection -> arguments 있으면 추가로 파싱해서 세팅 후 selections 재귀 호출 + - inline fragment -> selections를 재귀 호출 + - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출" + [{:keys [arguments fragment-name selections field-name leaf?] + :as _selection} fragment-map] + (match [(boolean leaf?) fragment-name] + [true _] nil + [false nil] (cond-> {:field-name field-name} + selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) + arguments (assoc :arguments arguments) + true vector) + [false nil] (mapcat #(parse-query % fragment-map) selections) + [false _] (let [{fragment-selections :selections} (fragment-name fragment-map)] + (mapcat #(parse-query % fragment-map) fragment-selections)))) + +(defn- count-nodes + "- pageInfo -> 0 반환 + - leaf -> n-nodes(pagination arg를 통해 조회될 resource 갯수) 반환 + - edges -> selections에 대해 재귀호출 후 합연산 + - connection o -> connection은 resource가 아니므로 연산에서 제외되어야 합니다. + -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해줍니다. + - connection x -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해준 결과에 n-nodes를 더해줍니다" + [{:keys [field-name selections arguments]}] + (let [{:keys [first last limit]} arguments + n-nodes (or first last limit 1) + leaf (seq selections) + connection? (->> selections + (remove (fn [{:keys [field-name]}] (#{:edges :pageInfo} field-name))) + empty?)] + (match [field-name connection? leaf] + [:pageInfo _ nil] 0 + [_ _ nil] n-nodes + [:edges _ _] (->> selections + (map #(count-nodes %)) + (reduce +)) + [_ true _] (->> selections + (map #(count-nodes %)) + (reduce +) + (* n-nodes)) + [_ false _] (->> selections + (map #(count-nodes %)) + (reduce +) + (* n-nodes) + (+ n-nodes))))) + +(defn complexity-analysis + [query {:keys [max-complexity] :as _options}] + (let [{:keys [fragments selections]} query + pq (mapcat #(parse-query % fragments) selections) + complexity (count-nodes pq)] + (when (and max-complexity (> complexity max-complexity)) + {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) From 6653697f1a44ceb17e9777de074fcb867cac53df Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 15 Jul 2024 18:55:21 +0900 Subject: [PATCH 02/21] add comment --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 096fad99..6cc07f63 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -9,12 +9,15 @@ [{:keys [arguments fragment-name selections field-name leaf?] :as _selection} fragment-map] (match [(boolean leaf?) fragment-name] + ;; for leaf field [true _] nil [false nil] (cond-> {:field-name field-name} selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) arguments (assoc :arguments arguments) true vector) + ;; for inline fragment [false nil] (mapcat #(parse-query % fragment-map) selections) + ;; for named fragment [false _] (let [{fragment-selections :selections} (fragment-name fragment-map)] (mapcat #(parse-query % fragment-map) fragment-selections)))) From 872194e6f0e92ccd44413310dd3ec48a9f8151c7 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Tue, 16 Jul 2024 11:18:10 +0900 Subject: [PATCH 03/21] apply review --- .../walmartlabs/lacinia/complexity_analysis.clj | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 6cc07f63..41cac14f 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -8,7 +8,7 @@ - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출" [{:keys [arguments fragment-name selections field-name leaf?] :as _selection} fragment-map] - (match [(boolean leaf?) fragment-name] + (match [leaf? fragment-name] ;; for leaf field [true _] nil [false nil] (cond-> {:field-name field-name} @@ -16,9 +16,9 @@ arguments (assoc :arguments arguments) true vector) ;; for inline fragment - [false nil] (mapcat #(parse-query % fragment-map) selections) + [nil nil] (mapcat #(parse-query % fragment-map) selections) ;; for named fragment - [false _] (let [{fragment-selections :selections} (fragment-name fragment-map)] + [nil _] (let [{fragment-selections :selections} (fragment-name fragment-map)] (mapcat #(parse-query % fragment-map) fragment-selections)))) (defn- count-nodes @@ -31,11 +31,11 @@ [{:keys [field-name selections arguments]}] (let [{:keys [first last limit]} arguments n-nodes (or first last limit 1) - leaf (seq selections) + leaf-node (seq selections) connection? (->> selections (remove (fn [{:keys [field-name]}] (#{:edges :pageInfo} field-name))) empty?)] - (match [field-name connection? leaf] + (match [field-name connection? leaf-node] [:pageInfo _ nil] 0 [_ _ nil] n-nodes [:edges _ _] (->> selections @@ -53,8 +53,9 @@ (defn complexity-analysis [query {:keys [max-complexity] :as _options}] - (let [{:keys [fragments selections]} query + (when max-complexity + (let [{:keys [fragments selections]} query pq (mapcat #(parse-query % fragments) selections) complexity (count-nodes pq)] - (when (and max-complexity (> complexity max-complexity)) - {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) + (when + {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)})))) From c1adf2b8f6c7822e40b60a0ef8fcc8ae533515aa Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Tue, 16 Jul 2024 11:20:36 +0900 Subject: [PATCH 04/21] nit --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 41cac14f..a8052814 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -57,5 +57,5 @@ (let [{:keys [fragments selections]} query pq (mapcat #(parse-query % fragments) selections) complexity (count-nodes pq)] - (when + (when (> complexity max-complexity) {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)})))) From 2b65f6a8aab71f660110e551a481eb77ea595d1d Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Fri, 19 Jul 2024 14:46:35 +0900 Subject: [PATCH 05/21] refactor --- .../lacinia/complexity_analysis.clj | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index a8052814..28fa9886 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,14 +1,25 @@ (ns com.walmartlabs.lacinia.complexity-analysis - (:require [clojure.core.match :refer [match]])) + (:require [clojure.core.match :refer [match]] + [com.walmartlabs.lacinia.internal-utils :refer [cond-let]] + [com.walmartlabs.lacinia.selection :as selection])) (defn- parse-query - "- leaf node -> nil + "- leaf field -> nil - field & selection -> arguments 있으면 추가로 파싱해서 세팅 후 selections 재귀 호출 - inline fragment -> selections를 재귀 호출 - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출" [{:keys [arguments fragment-name selections field-name leaf?] - :as _selection} fragment-map] - (match [leaf? fragment-name] + :as selection} fragment-map] + (cond + leaf? nil + (= :named-fragment (selection/selection-kind selection)) (let [{fragment-selections :selections} (fragment-name fragment-map)] + (mapcat #(parse-query % fragment-map) fragment-selections)) + (= :inline-fragment (selection/selection-kind selection)) (mapcat #(parse-query % fragment-map) selections) + :else (cond-> {:field-name field-name} + selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) + arguments (assoc :arguments arguments) + true vector)) + #_(match [leaf? fragment-name] ;; for leaf field [true _] nil [false nil] (cond-> {:field-name field-name} @@ -19,7 +30,7 @@ [nil nil] (mapcat #(parse-query % fragment-map) selections) ;; for named fragment [nil _] (let [{fragment-selections :selections} (fragment-name fragment-map)] - (mapcat #(parse-query % fragment-map) fragment-selections)))) + (mapcat #(parse-query % fragment-map) fragment-selections)))) (defn- count-nodes "- pageInfo -> 0 반환 @@ -28,7 +39,9 @@ - connection o -> connection은 resource가 아니므로 연산에서 제외되어야 합니다. -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해줍니다. - connection x -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해준 결과에 n-nodes를 더해줍니다" - [{:keys [field-name selections arguments]}] + [{:keys [field-name selections arguments]}] + #_(cond-let + (and (= field-name :pageInfo) )) (let [{:keys [first last limit]} arguments n-nodes (or first last limit 1) leaf-node (seq selections) @@ -53,9 +66,9 @@ (defn complexity-analysis [query {:keys [max-complexity] :as _options}] - (when max-complexity - (let [{:keys [fragments selections]} query + (let [{:keys [fragments selections]} query pq (mapcat #(parse-query % fragments) selections) complexity (count-nodes pq)] + (prn complexity pq) (when (> complexity max-complexity) - {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)})))) + {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) From 7ce8d07e402ffbc21c1d13e5e16013d716dac885 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Fri, 19 Jul 2024 15:52:41 +0900 Subject: [PATCH 06/21] nit --- src/com/walmartlabs/lacinia.clj | 5 +++-- .../lacinia/complexity_analysis.clj | 18 ++---------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/com/walmartlabs/lacinia.clj b/src/com/walmartlabs/lacinia.clj index 9cdf23b7..6ed496f9 100644 --- a/src/com/walmartlabs/lacinia.clj +++ b/src/com/walmartlabs/lacinia.clj @@ -58,8 +58,9 @@ (seq validation-errors) (resolve/resolve-as {:errors validation-errors}) - - :let [complexity-error (complexity-analysis/complexity-analysis prepared options)] + + :let [complexity-error (when (:max-complexity options) + (complexity-analysis/complexity-analysis prepared options))] (some? complexity-error) (resolve/resolve-as {:errors complexity-error}) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 28fa9886..6d42ffae 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -18,19 +18,7 @@ :else (cond-> {:field-name field-name} selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) arguments (assoc :arguments arguments) - true vector)) - #_(match [leaf? fragment-name] - ;; for leaf field - [true _] nil - [false nil] (cond-> {:field-name field-name} - selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) - arguments (assoc :arguments arguments) - true vector) - ;; for inline fragment - [nil nil] (mapcat #(parse-query % fragment-map) selections) - ;; for named fragment - [nil _] (let [{fragment-selections :selections} (fragment-name fragment-map)] - (mapcat #(parse-query % fragment-map) fragment-selections)))) + true vector))) (defn- count-nodes "- pageInfo -> 0 반환 @@ -39,9 +27,7 @@ - connection o -> connection은 resource가 아니므로 연산에서 제외되어야 합니다. -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해줍니다. - connection x -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해준 결과에 n-nodes를 더해줍니다" - [{:keys [field-name selections arguments]}] - #_(cond-let - (and (= field-name :pageInfo) )) + [{:keys [field-name selections arguments]}] (let [{:keys [first last limit]} arguments n-nodes (or first last limit 1) leaf-node (seq selections) From 34c2bf4c34bc5ede7a913a6a74f1bf844c779c19 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Fri, 19 Jul 2024 17:58:49 +0900 Subject: [PATCH 07/21] refactor match to cond --- .../lacinia/complexity_analysis.clj | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 6d42ffae..c65d09d2 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,9 +1,7 @@ (ns com.walmartlabs.lacinia.complexity-analysis - (:require [clojure.core.match :refer [match]] - [com.walmartlabs.lacinia.internal-utils :refer [cond-let]] - [com.walmartlabs.lacinia.selection :as selection])) + (:require [com.walmartlabs.lacinia.selection :as selection])) -(defn- parse-query +(defn^:private summarize-query "- leaf field -> nil - field & selection -> arguments 있으면 추가로 파싱해서 세팅 후 selections 재귀 호출 - inline fragment -> selections를 재귀 호출 @@ -13,16 +11,16 @@ (cond leaf? nil (= :named-fragment (selection/selection-kind selection)) (let [{fragment-selections :selections} (fragment-name fragment-map)] - (mapcat #(parse-query % fragment-map) fragment-selections)) - (= :inline-fragment (selection/selection-kind selection)) (mapcat #(parse-query % fragment-map) selections) + (mapcat #(summarize-query % fragment-map) fragment-selections)) + (= :inline-fragment (selection/selection-kind selection)) (mapcat #(summarize-query % fragment-map) selections) :else (cond-> {:field-name field-name} - selections (assoc :selections (mapcat #(parse-query % fragment-map) selections)) + selections (assoc :selections (mapcat #(summarize-query % fragment-map) selections)) arguments (assoc :arguments arguments) true vector))) -(defn- count-nodes +(defn^:private calculate-complexity "- pageInfo -> 0 반환 - - leaf -> n-nodes(pagination arg를 통해 조회될 resource 갯수) 반환 + - leaf-node -> n-nodes(pagination arg를 통해 조회될 resource 갯수) 반환 - edges -> selections에 대해 재귀호출 후 합연산 - connection o -> connection은 resource가 아니므로 연산에서 제외되어야 합니다. -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해줍니다. @@ -34,27 +32,26 @@ connection? (->> selections (remove (fn [{:keys [field-name]}] (#{:edges :pageInfo} field-name))) empty?)] - (match [field-name connection? leaf-node] - [:pageInfo _ nil] 0 - [_ _ nil] n-nodes - [:edges _ _] (->> selections - (map #(count-nodes %)) - (reduce +)) - [_ true _] (->> selections - (map #(count-nodes %)) - (reduce +) - (* n-nodes)) - [_ false _] (->> selections - (map #(count-nodes %)) + (cond + (= field-name :pageInfo) 0 + (nil? leaf-node) n-nodes + (= field-name :edges) (->> selections + (map #(calculate-complexity %)) + (reduce +)) + connection? (->> selections + (map #(calculate-complexity %)) (reduce +) - (* n-nodes) - (+ n-nodes))))) + (* n-nodes)) + (false? connection?) (->> selections + (map #(calculate-complexity %)) + (reduce +) + (* n-nodes) + (+ n-nodes))))) (defn complexity-analysis [query {:keys [max-complexity] :as _options}] (let [{:keys [fragments selections]} query - pq (mapcat #(parse-query % fragments) selections) - complexity (count-nodes pq)] - (prn complexity pq) + pq (mapcat #(summarize-query % fragments) selections) + complexity (calculate-complexity pq)] (when (> complexity max-complexity) {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) From 714b2e33d1a4d9ef2ff6e7a0f2e40ab904ca6854 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 22 Jul 2024 15:29:10 +0900 Subject: [PATCH 08/21] apply review --- .../lacinia/complexity_analysis.clj | 96 ++++---- .../lacinia/complexity_analysis_test.clj | 213 ++++++++++++++++++ 2 files changed, 263 insertions(+), 46 deletions(-) create mode 100644 test/com/walmartlabs/lacinia/complexity_analysis_test.clj diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index c65d09d2..78333662 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,57 +1,61 @@ (ns com.walmartlabs.lacinia.complexity-analysis - (:require [com.walmartlabs.lacinia.selection :as selection])) + (:require [com.walmartlabs.lacinia.selection :as selection] + [com.walmartlabs.lacinia.internal-utils :refer [cond-let]] + [clojure.walk])) -(defn^:private summarize-query + + +(declare ^:private summarize-selection) + +(defn summarize-selections + [selections fragment-map] + (mapcat #(summarize-selection % fragment-map) selections)) + +(defn summarize-field "- leaf field -> nil - - field & selection -> arguments 있으면 추가로 파싱해서 세팅 후 selections 재귀 호출 + - pageInfo -> nil + - edges -> + - else -> " + [{:keys [arguments selections field-name leaf?]} fragment-map] + (cond-let + leaf? nil + (= :pageInfo field-name) nil + (= :edges field-name) (summarize-field (first selections) fragment-map) + :let [n-nodes (-> arguments + (select-keys [:first :last :take :limit]) + vals + first) + n-nodes' (or n-nodes 1)] + + :else (-> {:field-name field-name} + (assoc :selections (summarize-selections selections fragment-map)) + (assoc :list-args? (some? n-nodes)) + (assoc :n-nodes n-nodes') + vector))) + +(defn ^:private summarize-selection + "- field -> summarize-field 에서 처리 - inline fragment -> selections를 재귀 호출 - - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출" - [{:keys [arguments fragment-name selections field-name leaf?] + - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출 " + [{:keys [fragment-name selections] :as selection} fragment-map] - (cond - leaf? nil - (= :named-fragment (selection/selection-kind selection)) (let [{fragment-selections :selections} (fragment-name fragment-map)] - (mapcat #(summarize-query % fragment-map) fragment-selections)) - (= :inline-fragment (selection/selection-kind selection)) (mapcat #(summarize-query % fragment-map) selections) - :else (cond-> {:field-name field-name} - selections (assoc :selections (mapcat #(summarize-query % fragment-map) selections)) - arguments (assoc :arguments arguments) - true vector))) - -(defn^:private calculate-complexity - "- pageInfo -> 0 반환 - - leaf-node -> n-nodes(pagination arg를 통해 조회될 resource 갯수) 반환 - - edges -> selections에 대해 재귀호출 후 합연산 - - connection o -> connection은 resource가 아니므로 연산에서 제외되어야 합니다. - -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해줍니다. - - connection x -> selections에 대해 재귀호출 후 합연산 후 n-nodes 만큼 곱해준 결과에 n-nodes를 더해줍니다" - [{:keys [field-name selections arguments]}] - (let [{:keys [first last limit]} arguments - n-nodes (or first last limit 1) - leaf-node (seq selections) - connection? (->> selections - (remove (fn [{:keys [field-name]}] (#{:edges :pageInfo} field-name))) - empty?)] - (cond - (= field-name :pageInfo) 0 - (nil? leaf-node) n-nodes - (= field-name :edges) (->> selections - (map #(calculate-complexity %)) - (reduce +)) - connection? (->> selections - (map #(calculate-complexity %)) - (reduce +) - (* n-nodes)) - (false? connection?) (->> selections - (map #(calculate-complexity %)) - (reduce +) - (* n-nodes) - (+ n-nodes))))) + (case (selection/selection-kind selection) + :field (summarize-field selection fragment-map) + :named-fragment (let [{fragment-selections :selections} (fragment-name fragment-map)] + (summarize-selections fragment-selections fragment-map)) + :inline-fragment (summarize-selections selections fragment-map))) + +(defn calculate-complexity + [{:keys [selections list-args? n-nodes]}] + (let [children-complexity (apply + (map calculate-complexity selections))] + (if list-args? + (* n-nodes children-complexity) + (+ n-nodes children-complexity)))) (defn complexity-analysis [query {:keys [max-complexity] :as _options}] (let [{:keys [fragments selections]} query - pq (mapcat #(summarize-query % fragments) selections) - complexity (calculate-complexity pq)] + pq (summarize-selections selections fragments) + complexity (calculate-complexity (first pq))] (when (> complexity max-complexity) {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) diff --git a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj new file mode 100644 index 00000000..e18604b6 --- /dev/null +++ b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj @@ -0,0 +1,213 @@ +(ns com.walmartlabs.lacinia.complexity-analysis-test + (:require [clojure.test :refer [deftest is testing]] + [com.walmartlabs.lacinia :refer [execute]] + [com.walmartlabs.lacinia.schema :as schema] + [com.walmartlabs.test-utils :refer [expect-exception]] + [com.walmartlabs.test-schema :refer [test-schema]])) + +;;------------------------------------------------------------------------------- +;; ## Tests + +(def compiled-schema (schema/compile test-schema)) + +(deftest temp-test + (testing "All leafs are scalar types or enums" + (let [q "{ hero }"] + (is (= {:errors [{:message "Field `Query/hero' must have at least one selection.", + :locations [{:line 1 + :column 3}]}]} + (execute compiled-schema q {} nil)))) + (let [q "{ hero { name friends } }"] + (is (= {:errors [{:message "Field `character/friends' must have at least one selection.", + :locations [{:line 1 + :column 15}]}]} + (execute compiled-schema q {} nil)))) + (let [q "query NestedQuery { + hero { + name + friends { + name + appears_in + friends + } + } + }"] + (is (= {:errors [{:message "Field `character/friends' must have at least one selection.", + :locations [{:column 18 + :line 7}]}]} + (execute compiled-schema q {} nil)))) + (let [q "query NestedQuery { + hero { + name + friends { + name + appears_in + friends { name + friends + } + } + } + }"] + (is (= {:errors [{:message "Field `character/friends' must have at least one selection." + :locations [{:column 28 + :line 8}]}]} + (execute compiled-schema q {} nil)))) + (let [q "query NestedQuery { + hero { + name + forceSide + friends { + friends + name + appears_in + forceSide + } + } + }"] + (is (= {:errors [{:locations [{:column 16 + :line 4}] + :message "Field `character/forceSide' must have at least one selection."} + {:locations [{:column 18 + :line 6}] + :message "Field `character/friends' must have at least one selection."} + {:locations [{:column 18 + :line 9}] + :message "Field `character/forceSide' must have at least one selection."}]} + (execute compiled-schema q {} nil)))) + (let [q "query NestedQuery { + hero { + name + forceSide + friends { + friends + name + appears_in + forceSide { name + members + } + } + } + }"] + (is (= {:errors [{:locations [{:column 16 + :line 4}] + :message "Field `character/forceSide' must have at least one selection."} + {:locations [{:column 18 + :line 6}] + :message "Field `character/friends' must have at least one selection."} + {:locations [{:column 30 + :line 10}] + :message "Field `force/members' must have at least one selection."}]} + (execute compiled-schema q {} nil)))) + (let [q "query NestedQuery { + hero { + name + forceSide + friends { + friends + name + appears_in + forceSide { name + members { name } + } + } + } + }"] + (is (= {:errors [{:locations [{:column 16 + :line 4}] + :message "Field `character/forceSide' must have at least one selection."} + {:locations [{:column 18 + :line 6}] + :message "Field `character/friends' must have at least one selection."}]} + (execute compiled-schema q {} nil)))) + (let [q "{ hero { name { id } } }"] + (is (= {:errors [{:extensions {:field-name :id} + :locations [{:column 17 + :line 1}] + :message "Path de-references through a scalar type."}]} + (execute compiled-schema q {} nil)))))) + +(deftest fragment-names-validations + (let [q "query UseFragment { + luke: human(id: \"1000\") { + ...HumanFragment + } + leia: human(id: \"1003\") { + ...HumanFragment + } + } + fragment HumanFragment on human { + name + homePlanet + }"] + (is (nil? (:errors (execute compiled-schema q {} nil))))) + (let [q "query UseFragment { + luke: human(id: \"1000\") { + ...FooFragment + } + leia: human(id: \"1003\") { + ...HumanFragment + } + } + fragment HumanFragment on human { + name + homePlanet + }"] + (is (= {:errors [{:message "Unknown fragment `FooFragment'. Fragment definition is missing." + :locations [{:line 3 + :column 19}]}]} + (execute compiled-schema q {} nil)))) + (let [q "query UseFragment { + luke: human(id: \"1000\") { + ...FooFragment + } + leia: human(id: \"1003\") { + ...BarFragment + } + } + fragment HumanFragment on human { + name + homePlanet + }"] + (is (= {:errors [{:locations [{:column 19 + :line 3}] + :message "Unknown fragment `FooFragment'. Fragment definition is missing."} + {:locations [{:column 19 + :line 6}] + :message "Unknown fragment `BarFragment'. Fragment definition is missing."} + {:locations [{:column 21 + :line 9}] + :message "Fragment `HumanFragment' is never used."}]} + (execute compiled-schema q {} nil)))) + (let [q "query withNestedFragments { + luke: human(id: \"1000\") { + friends { + ...friendFieldsFragment + } + } + } + fragment friendFieldsFragment on human { + id + name + ...appearsInFragment + } + fragment appearsInFragment on human { + appears_in + }"] + (is (nil? (:errors (execute compiled-schema q {} nil))))) + (let [q "query withNestedFragments { + luke: human(id: \"1000\") { + friends { + ...friendFieldsFragment + } + } + } + fragment friendFieldsFragment on human { + id + name + ...appearsInFragment + }"] + (is (= {:errors [{:message "Unknown fragment `appearsInFragment'. Fragment definition is missing." + :locations [{:line 11 :column 17}]}]} + (execute compiled-schema q {} nil))))) + + From c8c504c125e478b9c638312ab9c05c2a3c5edeee Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 22 Jul 2024 15:30:20 +0900 Subject: [PATCH 09/21] nit --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 78333662..c3757519 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -55,7 +55,7 @@ (defn complexity-analysis [query {:keys [max-complexity] :as _options}] (let [{:keys [fragments selections]} query - pq (summarize-selections selections fragments) - complexity (calculate-complexity (first pq))] + summarized-selections (summarize-selections selections fragments) + complexity (calculate-complexity (first summarized-selections))] (when (> complexity max-complexity) {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) From 4000dbdfc41cd7ceb74619d131c055c4ef694cd8 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 22 Jul 2024 17:24:05 +0900 Subject: [PATCH 10/21] remove --- .../lacinia/complexity_analysis_test.clj | 213 ------------------ 1 file changed, 213 deletions(-) delete mode 100644 test/com/walmartlabs/lacinia/complexity_analysis_test.clj diff --git a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj deleted file mode 100644 index e18604b6..00000000 --- a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj +++ /dev/null @@ -1,213 +0,0 @@ -(ns com.walmartlabs.lacinia.complexity-analysis-test - (:require [clojure.test :refer [deftest is testing]] - [com.walmartlabs.lacinia :refer [execute]] - [com.walmartlabs.lacinia.schema :as schema] - [com.walmartlabs.test-utils :refer [expect-exception]] - [com.walmartlabs.test-schema :refer [test-schema]])) - -;;------------------------------------------------------------------------------- -;; ## Tests - -(def compiled-schema (schema/compile test-schema)) - -(deftest temp-test - (testing "All leafs are scalar types or enums" - (let [q "{ hero }"] - (is (= {:errors [{:message "Field `Query/hero' must have at least one selection.", - :locations [{:line 1 - :column 3}]}]} - (execute compiled-schema q {} nil)))) - (let [q "{ hero { name friends } }"] - (is (= {:errors [{:message "Field `character/friends' must have at least one selection.", - :locations [{:line 1 - :column 15}]}]} - (execute compiled-schema q {} nil)))) - (let [q "query NestedQuery { - hero { - name - friends { - name - appears_in - friends - } - } - }"] - (is (= {:errors [{:message "Field `character/friends' must have at least one selection.", - :locations [{:column 18 - :line 7}]}]} - (execute compiled-schema q {} nil)))) - (let [q "query NestedQuery { - hero { - name - friends { - name - appears_in - friends { name - friends - } - } - } - }"] - (is (= {:errors [{:message "Field `character/friends' must have at least one selection." - :locations [{:column 28 - :line 8}]}]} - (execute compiled-schema q {} nil)))) - (let [q "query NestedQuery { - hero { - name - forceSide - friends { - friends - name - appears_in - forceSide - } - } - }"] - (is (= {:errors [{:locations [{:column 16 - :line 4}] - :message "Field `character/forceSide' must have at least one selection."} - {:locations [{:column 18 - :line 6}] - :message "Field `character/friends' must have at least one selection."} - {:locations [{:column 18 - :line 9}] - :message "Field `character/forceSide' must have at least one selection."}]} - (execute compiled-schema q {} nil)))) - (let [q "query NestedQuery { - hero { - name - forceSide - friends { - friends - name - appears_in - forceSide { name - members - } - } - } - }"] - (is (= {:errors [{:locations [{:column 16 - :line 4}] - :message "Field `character/forceSide' must have at least one selection."} - {:locations [{:column 18 - :line 6}] - :message "Field `character/friends' must have at least one selection."} - {:locations [{:column 30 - :line 10}] - :message "Field `force/members' must have at least one selection."}]} - (execute compiled-schema q {} nil)))) - (let [q "query NestedQuery { - hero { - name - forceSide - friends { - friends - name - appears_in - forceSide { name - members { name } - } - } - } - }"] - (is (= {:errors [{:locations [{:column 16 - :line 4}] - :message "Field `character/forceSide' must have at least one selection."} - {:locations [{:column 18 - :line 6}] - :message "Field `character/friends' must have at least one selection."}]} - (execute compiled-schema q {} nil)))) - (let [q "{ hero { name { id } } }"] - (is (= {:errors [{:extensions {:field-name :id} - :locations [{:column 17 - :line 1}] - :message "Path de-references through a scalar type."}]} - (execute compiled-schema q {} nil)))))) - -(deftest fragment-names-validations - (let [q "query UseFragment { - luke: human(id: \"1000\") { - ...HumanFragment - } - leia: human(id: \"1003\") { - ...HumanFragment - } - } - fragment HumanFragment on human { - name - homePlanet - }"] - (is (nil? (:errors (execute compiled-schema q {} nil))))) - (let [q "query UseFragment { - luke: human(id: \"1000\") { - ...FooFragment - } - leia: human(id: \"1003\") { - ...HumanFragment - } - } - fragment HumanFragment on human { - name - homePlanet - }"] - (is (= {:errors [{:message "Unknown fragment `FooFragment'. Fragment definition is missing." - :locations [{:line 3 - :column 19}]}]} - (execute compiled-schema q {} nil)))) - (let [q "query UseFragment { - luke: human(id: \"1000\") { - ...FooFragment - } - leia: human(id: \"1003\") { - ...BarFragment - } - } - fragment HumanFragment on human { - name - homePlanet - }"] - (is (= {:errors [{:locations [{:column 19 - :line 3}] - :message "Unknown fragment `FooFragment'. Fragment definition is missing."} - {:locations [{:column 19 - :line 6}] - :message "Unknown fragment `BarFragment'. Fragment definition is missing."} - {:locations [{:column 21 - :line 9}] - :message "Fragment `HumanFragment' is never used."}]} - (execute compiled-schema q {} nil)))) - (let [q "query withNestedFragments { - luke: human(id: \"1000\") { - friends { - ...friendFieldsFragment - } - } - } - fragment friendFieldsFragment on human { - id - name - ...appearsInFragment - } - fragment appearsInFragment on human { - appears_in - }"] - (is (nil? (:errors (execute compiled-schema q {} nil))))) - (let [q "query withNestedFragments { - luke: human(id: \"1000\") { - friends { - ...friendFieldsFragment - } - } - } - fragment friendFieldsFragment on human { - id - name - ...appearsInFragment - }"] - (is (= {:errors [{:message "Unknown fragment `appearsInFragment'. Fragment definition is missing." - :locations [{:line 11 :column 17}]}]} - (execute compiled-schema q {} nil))))) - - From 2c5ffd3d1fda39c548b322a621de3aa79387d866 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 22 Jul 2024 17:24:52 +0900 Subject: [PATCH 11/21] fix lint --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index c3757519..ab3a425a 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,9 +1,6 @@ (ns com.walmartlabs.lacinia.complexity-analysis (:require [com.walmartlabs.lacinia.selection :as selection] - [com.walmartlabs.lacinia.internal-utils :refer [cond-let]] - [clojure.walk])) - - + [com.walmartlabs.lacinia.internal-utils :refer [cond-let]])) (declare ^:private summarize-selection) From f95f0965bdf663a373c3ea8b496337c43e801ad5 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Mon, 22 Jul 2024 17:26:11 +0900 Subject: [PATCH 12/21] private --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index ab3a425a..c9c660be 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -4,11 +4,11 @@ (declare ^:private summarize-selection) -(defn summarize-selections +(defn ^:private summarize-selections [selections fragment-map] (mapcat #(summarize-selection % fragment-map) selections)) -(defn summarize-field +(defn ^:private summarize-field "- leaf field -> nil - pageInfo -> nil - edges -> @@ -42,7 +42,7 @@ (summarize-selections fragment-selections fragment-map)) :inline-fragment (summarize-selections selections fragment-map))) -(defn calculate-complexity +(defn ^:private calculate-complexity [{:keys [selections list-args? n-nodes]}] (let [children-complexity (apply + (map calculate-complexity selections))] (if list-args? From 6c5c2ef9c6df5cf5cbc419ceb9c8eb241b4914f9 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Thu, 8 Aug 2024 17:45:29 +0900 Subject: [PATCH 13/21] add test --- .../lacinia/complexity_analysis_test.clj | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/com/walmartlabs/lacinia/complexity_analysis_test.clj diff --git a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj new file mode 100644 index 00000000..8678b3f6 --- /dev/null +++ b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj @@ -0,0 +1,250 @@ +(ns com.walmartlabs.lacinia.complexity-analysis-test + (:require + [clojure.test :refer [deftest is]] + [com.walmartlabs.lacinia :refer [execute]] + [com.walmartlabs.lacinia.schema :as schema] + [com.walmartlabs.test-utils :as utils])) + + +(defn ^:private resolve-characters + [_ _ _] + [(schema/tag-with-type {:name "R2-D2" :power "AC"} :droid) + (schema/tag-with-type {:name "Luke" :home_world "Tatooine"} :human)]) + +(defn ^:private resolve-friends + [_ _ _] + [(schema/tag-with-type {:name "C3P0" :power "DC"} :droid) + (schema/tag-with-type {:name "Obi-Wan" :home_world "Stewjon"} :human)]) + +(def ^:private schema + (utils/compile-schema "fragments-.edn" + {:resolve-characters resolve-characters + :resolve-friends resolve-friends})) + +(defn ^:private q [query] + (utils/simplify (execute schema query nil nil))) + +(deftest inline-fragments + (is (= {:data {:characters [{:name "R2-D2" + :power "AC"} + {:name "Luke" + :home_world "Tatooine"}]}} + (q "{ characters { + name + ... on droid { power } + ... on human { home_world } + } + }")))) + +(deftest query-root-fragment + (is (= {:data {:characters [{:name "R2-D2" + :power "AC"} + {:name "Luke" + :home_world "Tatooine"}]}} + (q "{ ... on Query { + characters { + name + ... on droid { power } + ... on human { home_world } + } + } + }")))) + +(deftest later-fragments-do-not-override-earlier + (is (= {:data {:characters + ;; For droids, we get friends/name normally, and + ;; friends/home_world (via fragment) + [{:name "R2-D2" + :power "AC" + :friends [{:name "C3P0"} + {:name "Obi-Wan" :home_world "Stewjon"}]} + {:name "Luke" + ;; Luke is a human, only gets friends/name (the fragment + ;; doesn't trigger). + :friends [{:name "C3P0"} + {:name "Obi-Wan"}]}]}} + (q " + { + characters { + name + friends { name } + ... on droid { + power + friends { + ... on human { + home_world + } + } + } + } + } + }")))) + +(deftest named-fragments + (is (= {:data {:characters [{:name "R2-D2" + :power "AC"} + {:home_world "Tatooine" + :name "Luke"}]}} + + (q "query { + + characters { + + name + + ... droidFragment + ... humanFragment + } + } + + fragment droidFragment on droid { power } + fragment humanFragment on human { home_world } + + ")))) + +(deftest nested-fragments + (is (= {:data {:characters [{:name "R2-D2" + :power "AC"} + {:home_world "Tatooine" + :name "Luke"}]}} + + (q "query { + + characters { ... characterFragment } + + } + + fragment characterFragment on character { + + name + + ... droidFragment + ... humanFragment + + } + + fragment droidFragment on droid { power } + fragment humanFragment on human { home_world } + + ")))) + +(deftest detect-simple-cycle + (is (= {:errors [{:locations [{:column 16 + :line 9}] + :message "Fragment `droidFragment' is self-referential via field `friends', forming a cycle."}]} + (q " + query { + characters { ... droidFragment } + } + + fragment droidFragment on droid { + name + power + friends { # line 9 + ... droidFragment + } + } + ")))) + +(deftest detect-cycle-via-named-fragment + (is (= {:errors [{:locations [{:column 20 + :line 7}] + :message "Fragment `friendsFragment' is self-referential via named fragment `droidFragment', forming a cycle."} + {:locations [{:column 20 + :line 13}] + :message "Fragment `droidFragment' is self-referential via named fragment `friendsFragment', forming a cycle."}]} + (q " + query { + characters { ... droidFragment } + } + + fragment friendsFragment on character { + ... droidFragment # line 7 + } + + fragment droidFragment on droid { + name + power + ... friendsFragment # line 13 + } + ")))) + +(deftest detect-cycle-via-named-fragment + (is (= {:errors [{:locations [{:column 19 + :line 8}] + :message "Fragment `commonFragment' is self-referential via named fragment `friendsFragment', forming a cycle."} + {:locations [{:column 20 + :line 12}] + :message "Fragment `friendsFragment' is self-referential via named fragment `droidFragment', forming a cycle."} + {:locations [{:column 20 + :line 17}] + :message "Fragment `droidFragment' is self-referential via named fragment `commonFragment', forming a cycle."}]} + (q " + query { + characters { ... droidFragment } + } + + fragment commonFragment on character { + name + ...friendsFragment + } # line 8 + + fragment friendsFragment on character { + ... droidFragment # line 7 + } # line 12 + + fragment droidFragment on droid { + power + ... commonFragment + } # line 17 + ")))) + +(deftest detect-cycle-via-inline-fragment + (is (= {:errors [{:locations [{:column 23 + :line 8}] + :message "Fragment `commonFragment' is self-referential via inline fragment on type `droid', forming a cycle."} + {:locations [{:column 20 + :line 15}] + :message "Fragment `droidFragment' is self-referential via named fragment `commonFragment', forming a cycle."}]} + (q " + query { + characters { ... droidFragment } + } + + fragment commonFragment on character { + name + ... on droid { # line 8 + ...droidFragment + } + } + + fragment droidFragment on droid { + power + ... commonFragment # line 15 + } + ")))) + +(deftest fragment-on-undefined-type + (is (= {:errors [{:message "Fragment `MovieCharacter' references unknown type `NotDefined'." + :extensions {:line 8 :column 19}}]} + (q " + query { + characters { + ... MovieCharacter + } + } + + fragment MovieCharacter on NotDefined { + name + } + ")))) + +(deftest fragment-at-root + (is (= {:data {:characters [{:name "R2-D2"} + {:name "Luke"}]}} + (q " + query { ...All } + + fragment All on Query { + characters { name } + }")))) From 8886a39eb6ba00963183f004ad3ca7082bc2504d Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Wed, 21 Aug 2024 19:06:57 +0900 Subject: [PATCH 14/21] refactor --- .../lacinia/complexity_analysis.clj | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index c9c660be..2fcca8a6 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,46 +1,35 @@ (ns com.walmartlabs.lacinia.complexity-analysis - (:require [com.walmartlabs.lacinia.selection :as selection] - [com.walmartlabs.lacinia.internal-utils :refer [cond-let]])) + (:require [com.walmartlabs.lacinia.selection :as selection])) -(declare ^:private summarize-selection) +(defn- list-args? [arguments] + (some? (or (:first arguments) + (:last arguments)))) -(defn ^:private summarize-selections - [selections fragment-map] - (mapcat #(summarize-selection % fragment-map) selections)) +(defn- summarize-selection + "Recursively summarizes the selection, handling field, inline fragment, and named fragment." + [{:keys [arguments selections field-name leaf? fragment-name] :as selection} fragment-map] + (let [selection-kind (selection/selection-kind selection)] + (cond + ;; If it's a leaf node or `pageInfo`, return nil. + (or leaf? (= :pageInfo field-name)) + nil -(defn ^:private summarize-field - "- leaf field -> nil - - pageInfo -> nil - - edges -> - - else -> " - [{:keys [arguments selections field-name leaf?]} fragment-map] - (cond-let - leaf? nil - (= :pageInfo field-name) nil - (= :edges field-name) (summarize-field (first selections) fragment-map) - :let [n-nodes (-> arguments - (select-keys [:first :last :take :limit]) - vals - first) - n-nodes' (or n-nodes 1)] + ;; If it's a named fragment, look it up in the fragment-map and process its selections. + (= :named-fragment selection-kind) + (let [sub-selections (:selections (fragment-map fragment-name))] + (mapcat #(summarize-selection % fragment-map) sub-selections)) - :else (-> {:field-name field-name} - (assoc :selections (summarize-selections selections fragment-map)) - (assoc :list-args? (some? n-nodes)) - (assoc :n-nodes n-nodes') - vector))) + ;; If it's an inline fragment or `edges` field, process its selections. + (or (= :inline-fragment selection-kind) (= field-name :edges)) + (mapcat #(summarize-selection % fragment-map) selections) -(defn ^:private summarize-selection - "- field -> summarize-field 에서 처리 - - inline fragment -> selections를 재귀 호출 - - named fragment -> fragment-name으로 fragment-map 조회 후 해당 fragment의 selections를 재귀 호출 " - [{:keys [fragment-name selections] - :as selection} fragment-map] - (case (selection/selection-kind selection) - :field (summarize-field selection fragment-map) - :named-fragment (let [{fragment-selections :selections} (fragment-name fragment-map)] - (summarize-selections fragment-selections fragment-map)) - :inline-fragment (summarize-selections selections fragment-map))) + ;; Otherwise, handle a regular field with potential nested selections. + :else + (let [n-nodes (or (-> arguments (select-keys [:first :last]) vals first) 1)] + [{:field-name field-name + :selections (mapcat #(summarize-selection % fragment-map) selections) + :list-args? (list-args? arguments) + :n-nodes n-nodes}])))) (defn ^:private calculate-complexity [{:keys [selections list-args? n-nodes]}] @@ -52,7 +41,7 @@ (defn complexity-analysis [query {:keys [max-complexity] :as _options}] (let [{:keys [fragments selections]} query - summarized-selections (summarize-selections selections fragments) + summarized-selections (mapcat #(summarize-selection % fragments) selections) complexity (calculate-complexity (first summarized-selections))] (when (> complexity max-complexity) {:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)}))) From 66cc17f2627b39b05548b92141c088961edeca91 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Wed, 21 Aug 2024 19:39:41 +0900 Subject: [PATCH 15/21] add test schema edn --- dev-resources/complexity-analysis-error.edn | 81 +++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 dev-resources/complexity-analysis-error.edn diff --git a/dev-resources/complexity-analysis-error.edn b/dev-resources/complexity-analysis-error.edn new file mode 100644 index 00000000..b2e3f058 --- /dev/null +++ b/dev-resources/complexity-analysis-error.edn @@ -0,0 +1,81 @@ +{:objects + {:PageInfo + {:fields {:startCursor {:type (non-null String)} + :endCursor {:type (non-null String)} + :hasNextPage {:type (non-null Boolean)} + :hasPreviousPage {:type (non-null Boolean)}}} + + :Product + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :seller {:type (non-null :Seller)} + :reviews + {:type (non-null :ReviewConnection) + :args {:first {:type Int} + :last {:type Int}} + :resolve :resolve-reviews} + :likers + {:type (non-null :UserConnection) + :args {:first {:type Int} + :last {:type Int}} + :resolve :resolve-likers}}} + + :ProductConnection + {:implements [:Connection] + :fields {:edges {:type (non-null (list (non-null :ProductEdge)))}}} + + :ProductEdge + {:implements [:Edge] + :fields {:cursor {:type (non-null String)} + :node {:type (non-null :Product)}}} + + :ReviewConnection + {:implements [:Connection] + :fields {:edges {:type (non-null (list (non-null :ReviewEdge)))}}} + + :ReviewEdge + {:implements [:Edge] + :fields {:cursor {:type (non-null String)} + :node {:type (non-null :Review)}}} + + :Review + {:fields {:id {:type (non-null ID)} + :author {:type :Author} + :post {:type :Post}}} + + :Seller + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :name {:type (non-null String)} + :products + {:type (non-null :ProductConnection) + :args {:first {:type (non-null Int)}} + :resolve :resolve-products}}} + + :Buyer + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :name {:type (non-null String)} + :followings + {:type (non-null :UserConnection) + :args {:first {:type (non-null Int)}} + :resolve :resolve-followings}}} + + :UserConnection + {:implements [:Connection] + :fields {:edges {:type (non-null (list (non-null :UserEdge)))}}} + + :UserEdge + {:implements [:Edge] + :fields {:cursor {:type (non-null String)} + :node {:type (non-null :User)}}}} + + :interfaces + {:Node {:fields {:id {:type (non-null ID)}}} + :Edge {:fields {:cursor {:type (non-null String)} + :node {:type (non-null :Node)}}} + :Connection {:fields {:edges {:type (non-null (list (non-null :Edge)))} + :pageInfo {:type (non-null :PageInfo)}}}} + + :unions + {:User {:members [:Seller :Buyer]}}} From 6549c1930d955d8b0231f6d6cd0d90fd7b3dcfcf Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Wed, 21 Aug 2024 19:39:51 +0900 Subject: [PATCH 16/21] private --- src/com/walmartlabs/lacinia/complexity_analysis.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/walmartlabs/lacinia/complexity_analysis.clj b/src/com/walmartlabs/lacinia/complexity_analysis.clj index 2fcca8a6..17a6c1f5 100644 --- a/src/com/walmartlabs/lacinia/complexity_analysis.clj +++ b/src/com/walmartlabs/lacinia/complexity_analysis.clj @@ -1,11 +1,11 @@ (ns com.walmartlabs.lacinia.complexity-analysis (:require [com.walmartlabs.lacinia.selection :as selection])) -(defn- list-args? [arguments] +(defn ^:private list-args? [arguments] (some? (or (:first arguments) (:last arguments)))) -(defn- summarize-selection +(defn ^:private summarize-selection "Recursively summarizes the selection, handling field, inline fragment, and named fragment." [{:keys [arguments selections field-name leaf? fragment-name] :as selection} fragment-map] (let [selection-kind (selection/selection-kind selection)] From a5eb59bfa3a33d68f024313b69b8c54b56eb571c Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Wed, 21 Aug 2024 19:57:44 +0900 Subject: [PATCH 17/21] fix schema --- dev-resources/complexity-analysis-error.edn | 34 +++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/dev-resources/complexity-analysis-error.edn b/dev-resources/complexity-analysis-error.edn index b2e3f058..a21e3aa1 100644 --- a/dev-resources/complexity-analysis-error.edn +++ b/dev-resources/complexity-analysis-error.edn @@ -1,4 +1,8 @@ -{:objects +{:queries {:node + {:type (non-null :Node) + :args {:id {:type (non-null ID)}} + :resolve :resolve-node}} + :objects {:PageInfo {:fields {:startCursor {:type (non-null String)} :endCursor {:type (non-null String)} @@ -22,26 +26,29 @@ :ProductConnection {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :ProductEdge)))}}} + :fields {:edges {:type (non-null (list (non-null :ProductEdge)))} + :pageInfo {:type (non-null :PageInfo)}}} :ProductEdge {:implements [:Edge] :fields {:cursor {:type (non-null String)} - :node {:type (non-null :Product)}}} + :node {:type (non-null :Node)}}} :ReviewConnection {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :ReviewEdge)))}}} + :fields {:edges {:type (non-null (list (non-null :ReviewEdge)))} + :pageInfo {:type (non-null :PageInfo)}}} :ReviewEdge {:implements [:Edge] :fields {:cursor {:type (non-null String)} - :node {:type (non-null :Review)}}} + :node {:type (non-null :Node)}}} :Review - {:fields {:id {:type (non-null ID)} - :author {:type :Author} - :post {:type :Post}}} + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :author {:type (non-null :User)} + :product {:type (non-null :Product)}}} :Seller {:implements [:Node] @@ -49,7 +56,8 @@ :name {:type (non-null String)} :products {:type (non-null :ProductConnection) - :args {:first {:type (non-null Int)}} + :args {:first {:type Int} + :last {:type Int}} :resolve :resolve-products}}} :Buyer @@ -58,17 +66,19 @@ :name {:type (non-null String)} :followings {:type (non-null :UserConnection) - :args {:first {:type (non-null Int)}} + :args {:first {:type Int} + :last {:type Int}} :resolve :resolve-followings}}} :UserConnection {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :UserEdge)))}}} + :fields {:edges {:type (non-null (list (non-null :UserEdge)))} + :pageInfo {:type (non-null :PageInfo)}}} :UserEdge {:implements [:Edge] :fields {:cursor {:type (non-null String)} - :node {:type (non-null :User)}}}} + :node {:type (non-null :Node)}}}} :interfaces {:Node {:fields {:id {:type (non-null ID)}}} From 2ac359c6378208e10e6e3d3f972f12fac0c12535 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Wed, 21 Aug 2024 19:57:59 +0900 Subject: [PATCH 18/21] add test --- .../lacinia/complexity_analysis_test.clj | 311 +++++------------- 1 file changed, 78 insertions(+), 233 deletions(-) diff --git a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj index 8678b3f6..01f91bf2 100644 --- a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj +++ b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj @@ -1,250 +1,95 @@ -(ns com.walmartlabs.lacinia.complexity-analysis-test +; Copyright (c) 2017-present Walmart, Inc. +; +; Licensed under the Apache License, Version 2.0 (the "License") +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +(ns com.walmartlabs.lacinia.complexity-analysis-test (:require - [clojure.test :refer [deftest is]] + [clojure.test :refer [deftest is run-test]] [com.walmartlabs.lacinia :refer [execute]] - [com.walmartlabs.lacinia.schema :as schema] [com.walmartlabs.test-utils :as utils])) -(defn ^:private resolve-characters +(defn ^:private resolve-products [_ _ _] - [(schema/tag-with-type {:name "R2-D2" :power "AC"} :droid) - (schema/tag-with-type {:name "Luke" :home_world "Tatooine"} :human)]) + {:edges [] + :pageInfo {}}) -(defn ^:private resolve-friends +(defn ^:private resolve-followings [_ _ _] - [(schema/tag-with-type {:name "C3P0" :power "DC"} :droid) - (schema/tag-with-type {:name "Obi-Wan" :home_world "Stewjon"} :human)]) + {:edges [] + :pageInfo {}}) -(def ^:private schema - (utils/compile-schema "fragments-.edn" - {:resolve-characters resolve-characters - :resolve-friends resolve-friends})) - -(defn ^:private q [query] - (utils/simplify (execute schema query nil nil))) +(defn ^:private resolve-reviews + [_ _ _] + {:edges [] + :pageInfo {}}) -(deftest inline-fragments - (is (= {:data {:characters [{:name "R2-D2" - :power "AC"} - {:name "Luke" - :home_world "Tatooine"}]}} - (q "{ characters { - name - ... on droid { power } - ... on human { home_world } - } - }")))) +(defn ^:private resolve-likers + [_ _ _] + {:edges [] + :pageInfo {}}) -(deftest query-root-fragment - (is (= {:data {:characters [{:name "R2-D2" - :power "AC"} - {:name "Luke" - :home_world "Tatooine"}]}} - (q "{ ... on Query { - characters { - name - ... on droid { power } - ... on human { home_world } - } - } - }")))) +(defn ^:private resolve-node + [_ _ _] + {:edges [] + :pageInfo {}}) -(deftest later-fragments-do-not-override-earlier - (is (= {:data {:characters - ;; For droids, we get friends/name normally, and - ;; friends/home_world (via fragment) - [{:name "R2-D2" - :power "AC" - :friends [{:name "C3P0"} - {:name "Obi-Wan" :home_world "Stewjon"}]} - {:name "Luke" - ;; Luke is a human, only gets friends/name (the fragment - ;; doesn't trigger). - :friends [{:name "C3P0"} - {:name "Obi-Wan"}]}]}} - (q " - { - characters { - name - friends { name } - ... on droid { - power - friends { - ... on human { - home_world - } +(def ^:private schema + (utils/compile-schema "complexity-analysis-error.edn" + {:resolve-products resolve-products + :resolve-followings resolve-followings + :resolve-reviews resolve-reviews + :resolve-likers resolve-likers + :resolve-node resolve-node})) + +(defn ^:private q [query variables] + (utils/simplify (execute schema query variables nil {:max-complexity 10}))) + +(deftest over-complexity-analysis + (is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 22"}} + (q "query ProductDetail($productId: ID){ + node(id: $productId) { + ... on Product { + ...ProductLikersFragment + seller{ + id + products(first: 5){ + edges{ + node{ + id + } + } + } } - } - } - } - }")))) - -(deftest named-fragments - (is (= {:data {:characters [{:name "R2-D2" - :power "AC"} - {:home_world "Tatooine" - :name "Luke"}]}} - - (q "query { - - characters { - - name - - ... droidFragment - ... humanFragment - } - } - - fragment droidFragment on droid { power } - fragment humanFragment on human { home_world } - - ")))) - -(deftest nested-fragments - (is (= {:data {:characters [{:name "R2-D2" - :power "AC"} - {:home_world "Tatooine" - :name "Luke"}]}} - - (q "query { - - characters { ... characterFragment } - - } - - fragment characterFragment on character { - - name - - ... droidFragment - ... humanFragment - - } - - fragment droidFragment on droid { power } - fragment humanFragment on human { home_world } - - ")))) - -(deftest detect-simple-cycle - (is (= {:errors [{:locations [{:column 16 - :line 9}] - :message "Fragment `droidFragment' is self-referential via field `friends', forming a cycle."}]} - (q " - query { - characters { ... droidFragment } - } - - fragment droidFragment on droid { - name - power - friends { # line 9 - ... droidFragment + reviews(first: 5){ + edges{ + node{ + id + } + } + } + } } } - ")))) - -(deftest detect-cycle-via-named-fragment - (is (= {:errors [{:locations [{:column 20 - :line 7}] - :message "Fragment `friendsFragment' is self-referential via named fragment `droidFragment', forming a cycle."} - {:locations [{:column 20 - :line 13}] - :message "Fragment `droidFragment' is self-referential via named fragment `friendsFragment', forming a cycle."}]} - (q " - query { - characters { ... droidFragment } - } - - fragment friendsFragment on character { - ... droidFragment # line 7 - } - - fragment droidFragment on droid { - name - power - ... friendsFragment # line 13 - } - ")))) - -(deftest detect-cycle-via-named-fragment - (is (= {:errors [{:locations [{:column 19 - :line 8}] - :message "Fragment `commonFragment' is self-referential via named fragment `friendsFragment', forming a cycle."} - {:locations [{:column 20 - :line 12}] - :message "Fragment `friendsFragment' is self-referential via named fragment `droidFragment', forming a cycle."} - {:locations [{:column 20 - :line 17}] - :message "Fragment `droidFragment' is self-referential via named fragment `commonFragment', forming a cycle."}]} - (q " - query { - characters { ... droidFragment } - } - - fragment commonFragment on character { - name - ...friendsFragment - } # line 8 - - fragment friendsFragment on character { - ... droidFragment # line 7 - } # line 12 - - fragment droidFragment on droid { - power - ... commonFragment - } # line 17 - ")))) - -(deftest detect-cycle-via-inline-fragment - (is (= {:errors [{:locations [{:column 23 - :line 8}] - :message "Fragment `commonFragment' is self-referential via inline fragment on type `droid', forming a cycle."} - {:locations [{:column 20 - :line 15}] - :message "Fragment `droidFragment' is self-referential via named fragment `commonFragment', forming a cycle."}]} - (q " - query { - characters { ... droidFragment } - } - - fragment commonFragment on character { - name - ... on droid { # line 8 - ...droidFragment + fragment ProductLikersFragment on Product { + likers(first: 10){ + edges{ + node{ + id + } + } } - } - - fragment droidFragment on droid { - power - ... commonFragment # line 15 - } - ")))) - -(deftest fragment-on-undefined-type - (is (= {:errors [{:message "Fragment `MovieCharacter' references unknown type `NotDefined'." - :extensions {:line 8 :column 19}}]} - (q " - query { - characters { - ... MovieCharacter - } - } - - fragment MovieCharacter on NotDefined { - name - } - ")))) - -(deftest fragment-at-root - (is (= {:data {:characters [{:name "R2-D2"} - {:name "Luke"}]}} - (q " - query { ...All } + }" {:productId "1"})))) - fragment All on Query { - characters { name } - }")))) +(comment + (run-test over-complexity-analysis)) From a25afd8077b88ca8d0f6bcf84bfce12061c8866f Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Thu, 22 Aug 2024 11:14:55 +0900 Subject: [PATCH 19/21] fix schema --- dev-resources/complexity-analysis-error.edn | 162 ++++++++++---------- 1 file changed, 79 insertions(+), 83 deletions(-) diff --git a/dev-resources/complexity-analysis-error.edn b/dev-resources/complexity-analysis-error.edn index a21e3aa1..41a93474 100644 --- a/dev-resources/complexity-analysis-error.edn +++ b/dev-resources/complexity-analysis-error.edn @@ -1,91 +1,87 @@ -{:queries {:node - {:type (non-null :Node) - :args {:id {:type (non-null ID)}} - :resolve :resolve-node}} - :objects - {:PageInfo - {:fields {:startCursor {:type (non-null String)} - :endCursor {:type (non-null String)} - :hasNextPage {:type (non-null Boolean)} - :hasPreviousPage {:type (non-null Boolean)}}} - - :Product - {:implements [:Node] - :fields {:id {:type (non-null ID)} - :seller {:type (non-null :Seller)} - :reviews - {:type (non-null :ReviewConnection) - :args {:first {:type Int} - :last {:type Int}} - :resolve :resolve-reviews} - :likers - {:type (non-null :UserConnection) - :args {:first {:type Int} - :last {:type Int}} - :resolve :resolve-likers}}} +{:interfaces + {:Node {:fields {:id {:type (non-null ID)}}} + :Edge {:fields {:cursor {:type (non-null String)} + :node {:type (non-null :Node)}}} + :Connection {:fields {:edges {:type (non-null (list (non-null :Edge)))} + :pageInfo {:type (non-null :PageInfo)}}} + :User {:implements [:Node] + :fields {:id {:type (non-null ID)} + :name {:type (non-null String)}}}} + +:objects +{:PageInfo + {:fields {:startCursor {:type (non-null String)} + :endCursor {:type (non-null String)} + :hasNextPage {:type (non-null Boolean)} + :hasPreviousPage {:type (non-null Boolean)}}} - :ProductConnection - {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :ProductEdge)))} - :pageInfo {:type (non-null :PageInfo)}}} - - :ProductEdge - {:implements [:Edge] - :fields {:cursor {:type (non-null String)} - :node {:type (non-null :Node)}}} + :Product + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :seller {:type (non-null :Seller)} + :reviews + {:type (non-null :ReviewConnection) + :args {:first {:type Int}} + :resolve :resolve-reviews} + :likers + {:type (non-null :UserConnection) + :args {:first {:type Int + :default-value 5}} + :resolve :resolve-likers}}} - :ReviewConnection - {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :ReviewEdge)))} - :pageInfo {:type (non-null :PageInfo)}}} + :ProductEdge + {:implements [:Edge] + :fields {:cursor {:type (non-null String)} + :node {:type (non-null :Product)}}} - :ReviewEdge - {:implements [:Edge] - :fields {:cursor {:type (non-null String)} - :node {:type (non-null :Node)}}} + :ProductConnection + {:implements [:Connection] + :fields {:edges {:type (non-null (list (non-null :ProductEdge)))} + :pageInfo {:type (non-null :PageInfo)}}} - :Review - {:implements [:Node] - :fields {:id {:type (non-null ID)} - :author {:type (non-null :User)} - :product {:type (non-null :Product)}}} + :Review + {:implements [:Node] + :fields {:id {:type (non-null ID)} + :author {:type (non-null :User)} + :product {:type (non-null :Product)}}} - :Seller - {:implements [:Node] - :fields {:id {:type (non-null ID)} - :name {:type (non-null String)} - :products - {:type (non-null :ProductConnection) - :args {:first {:type Int} - :last {:type Int}} - :resolve :resolve-products}}} - - :Buyer - {:implements [:Node] - :fields {:id {:type (non-null ID)} - :name {:type (non-null String)} - :followings - {:type (non-null :UserConnection) - :args {:first {:type Int} - :last {:type Int}} - :resolve :resolve-followings}}} + :ReviewEdge + {:implements [:Edge] + :fields {:cursor {:type (non-null String)} + :node {:type (non-null :Review)}}} - :UserConnection - {:implements [:Connection] - :fields {:edges {:type (non-null (list (non-null :UserEdge)))} - :pageInfo {:type (non-null :PageInfo)}}} + :ReviewConnection + {:implements [:Connection] + :fields {:edges {:type (non-null (list (non-null :ReviewEdge)))} + :pageInfo {:type (non-null :PageInfo)}}} - :UserEdge - {:implements [:Edge] - :fields {:cursor {:type (non-null String)} - :node {:type (non-null :Node)}}}} - - :interfaces - {:Node {:fields {:id {:type (non-null ID)}}} - :Edge {:fields {:cursor {:type (non-null String)} - :node {:type (non-null :Node)}}} - :Connection {:fields {:edges {:type (non-null (list (non-null :Edge)))} - :pageInfo {:type (non-null :PageInfo)}}}} + :Seller + {:implements [:Node :User] + :fields {:id {:type (non-null ID)} + :name {:type (non-null String)} + :products + {:type (non-null :ProductConnection) + :args {:first {:type Int}} + :resolve :resolve-products}}} + + :Buyer + {:implements [:Node :User] + :fields {:id {:type (non-null ID)} + :name {:type (non-null String)} + :followings + {:type (non-null :UserConnection) + :args {:first {:type Int}} + :resolve :resolve-followings}}} - :unions - {:User {:members [:Seller :Buyer]}}} + :UserEdge + {:fields {:cursor {:type (non-null String)} + :node {:type (non-null :User)}}} + + :UserConnection + {:fields {:edges {:type (non-null (list (non-null :UserEdge)))} + :pageInfo {:type (non-null :PageInfo)}}}} + + :queries {:node + {:type :Node + :args {:id {:type (non-null ID)}} + :resolve :resolve-node}}} From 98b0c93dbecf01b431cab1af3c31cca42a4a3e4f Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Thu, 22 Aug 2024 11:15:02 +0900 Subject: [PATCH 20/21] add test --- .../lacinia/complexity_analysis_test.clj | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj index 01f91bf2..90b494d1 100644 --- a/test/com/walmartlabs/lacinia/complexity_analysis_test.clj +++ b/test/com/walmartlabs/lacinia/complexity_analysis_test.clj @@ -14,7 +14,7 @@ (ns com.walmartlabs.lacinia.complexity-analysis-test (:require - [clojure.test :refer [deftest is run-test]] + [clojure.test :refer [deftest is run-test testing]] [com.walmartlabs.lacinia :refer [execute]] [com.walmartlabs.test-utils :as utils])) @@ -56,8 +56,10 @@ (utils/simplify (execute schema query variables nil {:max-complexity 10}))) (deftest over-complexity-analysis - (is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 22"}} - (q "query ProductDetail($productId: ID){ + (testing "It is possible to calculate the complexity of a query in the Relay connection spec + by taking into account both named fragments and inline fragments." + (is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 27"}} + (q "query ProductDetail($productId: ID){ node(id: $productId) { ... on Product { ...ProductLikersFragment @@ -75,6 +77,9 @@ edges{ node{ id + author{ + id + } } } } @@ -85,11 +90,59 @@ likers(first: 10){ edges{ node{ - id + ... on Seller{ + id + } + ... on Buyer{ + id + } } } } - }" {:productId "1"})))) + }" {:productId "id"})))) + (testing "If no arguments are passed in the query, the calculation uses the default value defined in the schema." + (is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 22"}} + (q "query ProductDetail($productId: ID){ + node(id: $productId) { + ... on Product { + ...ProductLikersFragment + seller{ + id + products(first: 5){ + edges{ + node{ + id + } + } + } + } + reviews(first: 5){ + edges{ + node{ + id + author{ + id + } + } + } + } + } + } + } + fragment ProductLikersFragment on Product { + likers{ + edges{ + node{ + ... on Seller{ + id + } + ... on Buyer{ + id + } + } + } + } + }" {:productId "id"}))))) (comment (run-test over-complexity-analysis)) From 279b65ede3a070006704a5e03d9a33d641828699 Mon Sep 17 00:00:00 2001 From: Gyeongmin Park Date: Thu, 22 Aug 2024 11:22:30 +0900 Subject: [PATCH 21/21] remove interface implement --- dev-resources/complexity-analysis-error.edn | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-resources/complexity-analysis-error.edn b/dev-resources/complexity-analysis-error.edn index 41a93474..851bc1bd 100644 --- a/dev-resources/complexity-analysis-error.edn +++ b/dev-resources/complexity-analysis-error.edn @@ -4,8 +4,7 @@ :node {:type (non-null :Node)}}} :Connection {:fields {:edges {:type (non-null (list (non-null :Edge)))} :pageInfo {:type (non-null :PageInfo)}}} - :User {:implements [:Node] - :fields {:id {:type (non-null ID)} + :User {:fields {:id {:type (non-null ID)} :name {:type (non-null String)}}}} :objects