From 6c5bd5f9814ac074b1a777dfb875daa9c1d079fc Mon Sep 17 00:00:00 2001 From: Valentin Waeselynck Date: Wed, 10 Oct 2018 13:37:51 +0200 Subject: [PATCH] moved to deps.edn + minor cleanup and improvements + fixed some tests --- .gitignore | 1 + deps.edn | 14 + project.clj | 17 +- src/d2q/api.clj | 5 +- src/d2q/impl.clj | 22 +- test/d2q/test/api.clj | 514 ++++++++---------------------- test/d2q/test/example/persons.clj | 3 +- 7 files changed, 176 insertions(+), 400 deletions(-) create mode 100644 deps.edn diff --git a/.gitignore b/.gitignore index a9453ec..274c935 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ pom.xml.asc *.iml .idea +.cpcache \ No newline at end of file diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..e82cb23 --- /dev/null +++ b/deps.edn @@ -0,0 +1,14 @@ +{:deps + {org.clojure/clojure {:mvn/version "1.8.0"} + manifold {:mvn/version "0.1.6"} + + com.taoensso/tufte {:mvn/version "1.4.0"} ;; FIXME + } + :aliases + {:dev + {:extra-deps + {midje {:mvn/version "1.7.0"} + vvvvalvalval/scope-capture {:mvn/version "0.1.4"} + vvvvalvalval/scope-capture-nrepl {:mvn/version "0.2.0"} + criterium {:mvn/version "0.4.4"} + vvvvalvalval/supdate {:mvn/version "0.2.1"}}}}} diff --git a/project.clj b/project.clj index 4589b21..7cc4f04 100644 --- a/project.clj +++ b/project.clj @@ -3,15 +3,12 @@ :url "https://github.com/vvvvalvalval/d2q" :license {:name "MIT License" :url "https://opensource.org/licenses/MIT"} - :dependencies [[org.clojure/clojure "1.8.0"] - [manifold "0.1.6"]] :profiles {:dev - {:dependencies - [[midje "1.7.0"] - [vvvvalvalval/scope-capture "0.1.4"] - [vvvvalvalval/scope-capture-nrepl "0.2.0"] - [criterium "0.4.4"] - [com.taoensso/tufte "1.4.0"] - [vvvvalvalval/supdate "0.2.1"]] - :repl-options {:nrepl-middleware [sc.nrepl.middleware/wrap-letsc]}}}) + {:lein-tools-deps/config {:aliases [:dev]} + :repl-options {:nrepl-middleware [sc.nrepl.middleware/wrap-letsc]}}} + + :plugins [[lein-tools-deps "0.4.1"]] + :middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn] + :lein-tools-deps/config {:config-files [:install :user :project]} + ) diff --git a/src/d2q/api.clj b/src/d2q/api.clj index 05cec5a..332ee91 100644 --- a/src/d2q/api.clj +++ b/src/d2q/api.clj @@ -12,13 +12,14 @@ ;; 2. √ Subquery preview in resolvers? May require to change the format of resolvers. ;; 3. √ Partial errors => resolvers return a map with {:data :errors} key ;; 4. Maybe skip the normalization of query - should be done by the caller -;; 5. Mutual recursion (requires to change the query format) +;; 5. Mutual recursion (requires to change the query format - but how? specified at the Field Call or the Query ?) ;; 6. Source mapping - keep track of (reversed) query-path and data-path ;; 7. √ Maybe faster processing via records ? ;; 8. √ Early subquery substitution -;; 9. Support union queries ? +;; 9. Support Conditional Queries ? - dispatch based on an attribute, applying queries that will be applied only to a subset of the entities ;; 10. Field meta ;; 11. Post process entities e.g have a function (ent-cell, result-map) -> result-map +;; 12. 'Flattening' / 'merging' for cardinality-one, ref-typed attributes ;; ------------------------------------------------------------------------------ ;; Server-side API diff --git a/src/d2q/impl.clj b/src/d2q/impl.clj index 688dda0..286398a 100644 --- a/src/d2q/impl.clj +++ b/src/d2q/impl.clj @@ -82,9 +82,11 @@ (letfn [(query* [svr, qctx, query, ent-sel q-trace] - (let [query-q-trace (cons [:d2q.trace.op/query - ;; NOTE elision to get better REPL output - (assoc query :d2q-query-fcalls [:...elided])] q-trace) + (assert (list? q-trace)) + (let [query-q-trace (conj q-trace + [:d2q.trace.op/query + ;; NOTE elision to get better REPL output + (assoc query :d2q-query-fcalls [:...elided])]) n-ents (count ent-sel)] (if (zero? n-ents) (mfd/success-deferred {:d2q-results [] :d2q-errors []}) @@ -116,7 +118,6 @@ ] (try (tp/field-table-resolver field) (catch Throwable err - (sc.api/spy ) (throw err))))) fcalls))] (-> @@ -145,10 +146,11 @@ (apply-resolver [svr qctx t-ent-sel resolver fcalls q-trace] + (assert (list? q-trace)) (let [resolver-q-trace - (cons - [:d2q.trace.op/resolver {:d2q.resolver/name (tp/tr-name resolver)}] - q-trace) + (conj + q-trace + [:d2q.trace.op/resolver {:d2q.resolver/name (tp/tr-name resolver)}]) fcall-tuples (p :fcall-tuples (into [] @@ -269,12 +271,12 @@ (map (fn [[^ArrayList res-cells, field, fcall]] (let [fck (:d2q-fcall-key fcall) subq (:d2q-fcall-subquery fcall) - fc-q-stack (cons + fc-q-stack (conj + resolver-q-trace [:d2q.trace.op/field-call ;; NOTE elision to get better REPL output (Val, 10 Apr 2018) (assoc fcall - :d2q-fcall-subquery [:...elided])] - resolver-q-trace)] + :d2q-fcall-subquery [:...elided])])] (if (tp/field-many? field) ;; to-many case (let [subsel diff --git a/test/d2q/test/api.clj b/test/d2q/test/api.clj index b559bf5..b1e0517 100644 --- a/test/d2q/test/api.clj +++ b/test/d2q/test/api.clj @@ -38,20 +38,20 @@ :synth.fields/return-arg fcall-arg :synth.fields/inc-arg (inc fcall-arg) :synth.fields/missing MISSING - :synth.fields/throw-arg (throw fcall-arg) + :synth.fields/throw-arg (throw (apply ex-info fcall-arg)) :synth.fields/qctx qctx :synth.fields.refs/one-child (->SynthEnt (str (:ent-id ent) "/" fcall-arg)) :synth.fields.refs/one-missing MISSING - :synth.fields.refs/one-throw-arg (throw fcall-arg) + :synth.fields.refs/one-throw-arg (throw (apply ex-info fcall-arg)) :synth.fields.refs/many-children (into [] (map (fn [i] (->SynthEnt (str (:ent-id ent) "/" i)))) (range fcall-arg)) :synth.fields.refs/many-missing MISSING - :synth.fields.refs/many-throw-arg (throw fcall-arg))) + :synth.fields.refs/many-throw-arg (throw (apply ex-info fcall-arg)))) (defn synthetic-resolve [qctx fcall-tuples ent-sel] @@ -78,7 +78,7 @@ [1 (d2q.datatypes/->FieldCall :synth.fields/return-arg :return-arg "hello" nil nil)] [2 (d2q.datatypes/->FieldCall :synth.fields.refs/one-child :one-child "a" nil nil)] [3 (d2q.datatypes/->FieldCall :synth.fields/missing :missing nil nil nil)] - [4 (d2q.datatypes/->FieldCall :synth.fields/throw-arg :throw-arg (ex-info "" {:error-tag "FZDSgjeizo"}) nil nil)] + [4 (d2q.datatypes/->FieldCall :synth.fields/throw-arg :throw-arg ["" {:error-tag "FZDSgjeizo"}] nil nil)] [5 (d2q.datatypes/->FieldCall :synth.fields/qctx :qctx nil nil nil)] ] [[0 (->SynthEnt "a")] @@ -193,24 +193,25 @@ :d2q.resolver/compute #'synthetic-resolve} {:d2q.resolver/name :synth.resolvers/throwing-resolver :d2q.resolver/compute - (fn [qctx [[fcall-i {err :d2q-fcall-arg}]] i+ents] - (throw err))} + (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] + (throw (ex-info msg data)))} {:d2q.resolver/name :synth.resolvers/error-deferred-resolver :d2q.resolver/compute - (fn [qctx [[fcall-i {err :d2q-fcall-arg}]] i+ents] - (mfd/error-deferred err))} + (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] + (mfd/error-deferred (ex-info msg data)))} {:d2q.resolver/name :synth.resolvers/error-returning-resolver :d2q.resolver/compute - (fn [qctx [[fcall-i {err :d2q-fcall-arg}]] i+ents] - (mfd/success-deferred {:d2q-errors [err]}))}]) + (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] + (mfd/success-deferred {:d2q-errors [(ex-info msg data)]}))}]) (defn synthetic-transform-entities [qctx q ent-sel] (mfd/success-deferred (->> ent-sel (map (fn [[ent-i ent :as cell]] - (if-let [err (:synth.ent/throw-at-transform-entities ent)] - {:d2q-errors err} + (if-let [[msg data] (:synth.ent/throw-at-transform-entities ent)] + (let [err (ex-info msg data)] + {:d2q-errors err}) (if-let [res (:synth.ent/early-result ent)] {:d2q-early-results {:d2q-entcell-i ent-i :d2q-rescell-value res}} {:d2q-ent-selection cell})))) @@ -226,48 +227,6 @@ :d2q.server/transform-entities-fn synthetic-transform-entities})) -[{:special/some-video - [{:comments [:comment/title]}]}] - -{:d2q-query-fcalls ;; [nil] - [{:d2q-fcall-field :special/some-video - :d2q-fcall-subquery - {:d2q-query-fcalls ;; [{:video/id "fjdslfjdlsk}] - [{:d2q-fcall-field :comments - :d2q-fcall-subquery - [:comment/title]}]}}]} - -( env - [[0 {:video/id "fdksfjdslkfjds"}]] - [[0 {:d2q-fcall-field :comments - :d2q-fcall-subquery - [:comment/title]}]]) -=> -(mfd/success-deferred - [[0 0 [{:comment/id "jfkdslfjdls"} {:comment/id "dsfjdsflsdkfjs"}]]]) - -(defn resolve-vieo-comments - [env i+fcalls j+ents] - (mfd/future - )) - - -:comment/description -:comment/title -:comment/date -(defn resolve-comments-fields - [env i+fcalls j+ents] - (mfd/future - (d/q - '[:find ?i ?j ?v :in [[?i ?attr]] [[?j ?comment]] - :where - [?comment ?attr ?v]] - (:db env) - i+fcalls - j+ents))) - - - (fact "Synthetic query example" (let [qctx nil q [:synth.fields/ent-id @@ -285,11 +244,11 @@ :d2q-fcall-key "scalar-k1" :d2q-fcall-arg "foo"} {:d2q-fcall-field :synth.fields.refs/many-throw-arg - :d2q-fcall-arg (ex-info "aaaaa" {:u "FDZVZVVZ"})} + :d2q-fcall-arg ["aaaaa" {:u "FDZVZVVZ"}]} {:d2q-fcall-field :synth.fields.refs/one-throw-arg - :d2q-fcall-arg (ex-info "bbbbbb" {:u 136})}]} + :d2q-fcall-arg ["bbbbbb" {:u 136}]}]} {:d2q-fcall-field :synth.fields/throw-arg - :d2q-fcall-arg (ex-info "nooooo" {:z 342522})} + :d2q-fcall-arg ["nooooo" {:z 342522}]} {:d2q-fcall-field :synth.fields.refs/many-children :d2q-fcall-key "children2" :d2q-fcall-arg 3 @@ -302,17 +261,17 @@ :synth.fields.refs/one-missing :synth.fields.refs/many-missing]} {:d2q-fcall-field :synth.fields/resolver-throws - :d2q-fcall-arg (ex-info "Resolver threw" {:d2q-fcall-i 0}) + :d2q-fcall-arg ["I threw an error during resolution" {:d2q-fcall-i 0}] :d2q-fcall-key "resolver-threw"} {:d2q-fcall-field :synth.fields/resolvers-returns-error - :d2q-fcall-arg (ex-info "Resolver returned error" {}) + :d2q-fcall-arg ["I returned an error during resolution" {}] :d2q-fcall-key "resolver-returned-error"} {:d2q-fcall-field :synth.fields/resolver-deferred-error - :d2q-fcall-arg (ex-info "Resolver returned error Deferred" {}) + :d2q-fcall-arg ["I returned an error Deferred during resolution" {}] :d2q-fcall-key "resolver-error-deferred"}] ents [(->SynthEnt 0) (assoc (->SynthEnt 1) - :synth.ent/throw-at-transform-entities (ex-info "jfdksl" {:x 34242}))]] + :synth.ent/throw-at-transform-entities ["jfdksl" {:x 34242}])]] (fact "Empty entities" (-> @(d2q.api/query (synth-server) qctx q []) @@ -327,8 +286,13 @@ :d2q-errors [{:error/type clojure.lang.ExceptionInfo, :error/message "Error in d2q Transform-Entities phase.", :error/data {:d2q.error/type :d2q.error.type/transform-entities, - :d2q.error/query-trace ([:d2q.trace.op/query :...elided])}, - :error/cause {:error/type clojure.lang.ExceptionInfo, :error/message "jfdksl", :error/data {:x 34242}}}]} + :d2q.error/query-trace ([:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, + :error/message "jfdksl", + :error/data {:x 34242}}}]} ) ;; FIXME fix tests for new error reporting (Val, 10 Apr 2018) @@ -345,18 +309,118 @@ {:synth.fields/ent-id "0/2", "scalar-k2" "bar", :synth.fields/always-42 42}]} {}], :d2q-errors [{:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/error-deferred-resolver", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/error-deferred-resolver, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/error-deferred-resolver}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, + :error/message "I returned an error Deferred during resolution", + :error/data {}}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/error-returning-resolver", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/error-returning-resolver, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/error-returning-resolver}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, + :error/message "I returned an error during resolution", + :error/data {}}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/resolver-1", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/resolver-1, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/resolver-1}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path (:d2q-fcall-subquery 4)}] + [:d2q.trace.op/field-call + #d2q.datatypes.FieldCall{:d2q-fcall-field :synth.fields.refs/one-child, + :d2q-fcall-key "child1", + :d2q-fcall-arg nil, + :d2q-fcall-subquery [:...elided], + :d2q-fcall-rev-query-path (4)}] + [:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/resolver-1}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, :error/message "bbbbbb", :error/data {:u 136}}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/resolver-2", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/resolver-2, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/resolver-2}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path (:d2q-fcall-subquery 4)}] + [:d2q.trace.op/field-call + #d2q.datatypes.FieldCall{:d2q-fcall-field :synth.fields.refs/one-child, + :d2q-fcall-key "child1", + :d2q-fcall-arg nil, + :d2q-fcall-subquery [:...elided], + :d2q-fcall-rev-query-path (4)}] + [:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/resolver-1}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, + :error/message "aaaaa", + :error/data {:u "FDZVZVVZ"}}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/resolver-2", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/resolver-2, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/resolver-2}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, :error/message "nooooo", :error/data {:z 342522}}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Error in d2q Resolver :synth.resolvers/throwing-resolver, on Field :synth.fields/resolver-throws at key \"resolver-threw\"", + :error/data {:d2q.error/type :d2q.error.type/resolver, + :d2q.resolver/name :synth.resolvers/throwing-resolver, + :d2q.error/query-trace ([:d2q.trace.op/resolver + {:d2q.resolver/name :synth.resolvers/throwing-resolver}] + [:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}]), + :d2q.error/field-call #d2q.datatypes.FieldCall{:d2q-fcall-field :synth.fields/resolver-throws, + :d2q-fcall-key "resolver-threw", + :d2q-fcall-arg ["I threw an error during resolution" + {:d2q-fcall-i 0}], + :d2q-fcall-subquery nil, + :d2q-fcall-rev-query-path (7)}}, + :error/cause {:error/type clojure.lang.ExceptionInfo, + :error/message "I threw an error during resolution", + :error/data {:d2q-fcall-i 0}}} + {:error/type clojure.lang.ExceptionInfo, :error/message "Error in d2q Transform-Entities phase.", :error/data {:d2q.error/type :d2q.error.type/transform-entities, - :d2q.error/query-trace ([:d2q.trace.op/query :...elided])}, - :error/cause {:error/type clojure.lang.ExceptionInfo, :error/message "jfdksl", :error/data {:x 34242}}} - {:error/type clojure.lang.ExceptionInfo, :error/message "Resolver returned error", :error/data {}} - {:error/type clojure.lang.ExceptionInfo, - :error/message "Resolver returned error Deferred", - :error/data {}} - {:error/type clojure.lang.ExceptionInfo, :error/message "Resolver threw", :error/data {}} - {:error/type clojure.lang.ExceptionInfo, :error/message "aaaaa", :error/data {:u "FDZVZVVZ"}} - {:error/type clojure.lang.ExceptionInfo, :error/message "bbbbbb", :error/data {:u 136}} - {:error/type clojure.lang.ExceptionInfo, :error/message "nooooo", :error/data {:z 342522}}]} + :d2q.error/query-trace ([:d2q.trace.op/query + #d2q.datatypes.Query{:d2q-query-id nil, + :d2q-query-fcalls [:...elided], + :d2q-rev-query-path ()}])}, + :error/cause {:error/type clojure.lang.ExceptionInfo, :error/message "jfdksl", :error/data {:x 34242}}}]} + ) @@ -415,307 +479,3 @@ :error/message "Divide by zero", :error/data nil}}]}) ) - - - -(comment ;; TODO DEPRECATED - - (def db-example - {:db/persons - {"john-doe" - {:person/id "john-doe" - :person/email "john.doe@gmail.com" - :person/age 18 - :person/address {:address/number "48" - :address/street "rue de Rome" - :address/city "Paris"} - :person/notes ["blah" :blah 42] - :animal/loves #{"alice-hacker" "minou"}} - "alice-hacker" - {:person/id "alice-hacker" - :person/email "alice.hacker@gmail.com" - :person/gender :person.gender/female - :person/notes [] - :person/address nil} - "bob-moran" - {:person/id "bob-moran" - :person/email "bob.moran@gmail.com" - :person/age nil - :person/gender :person.gender/male - :person/address {:address/number "17" - :address/street "rue de Mars" - :address/city "Orléans"}}} - :db/cats - {"minou" - {:cat/id "minou" - :cat/name "Minou" - :cat/owner "john-doe" - :animal/loves #{"john-doe" "alice-hacker" "fuzzy-fur"}} - "fuzzy-fur" - {:cat/id "fuzzy-fur" - :cat/name "Fuzzy Fur" - :cat/owner "bob-moran" - :animal/loves #{}} - "wild-cat" - {:cat/id "wild-cat" - :cat/name "Wild Cat" - :animal/loves #{}}}} - ) - - (defn qctx-example - "Constructs an example Query Context." - [] - {:db db-example}) - - ;; ------------------------------------------------------------------------------ - ;; Reads - - (defn- basic-fr - "Concise helper for defining field resolvers" - ([field-name ref? many? doc] - (basic-fr field-name ref? many? doc - (if ref? - (throw (ex-info "Cannot create compute function for entity-typed FR" {:d2q.field/name field-name})) - (fn [_ obj _ _] - (get obj field-name))))) - ([field-name ref? many? doc compute] - {:d2q.field/name field-name - :doc doc - :d2q.field/ref? ref? - :d2q.field/cardinality (if many? :d2q.field.cardinality/many :d2q.field.cardinality/one) - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - compute})) - - (def field-resolvers - [{:d2q.field/name :find-person-by-id - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/one - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [qctx obj _ [person-id]] - (when-let [p (get-in (:db qctx) [:db/persons person-id])] - p))} - {:d2q.field/name :find-cat-by-id - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/one - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [qctx obj _ [cat-id]] - (when-let [c (get-in (:db qctx) [:db/cats cat-id])] - c))} - {:d2q.field/name :animal/loves - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [qctx obj _ _] - (let [{cats :db/cats persons :db/persons} (:db qctx)] - (->> obj :animal/loves set - (mapv (fn [id] - (or - (get cats id) - (get persons id))))) - ))} - {:d2q.field/name :animal/loved-by - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [qctx obj _ _] - (let [{cats :db/cats persons :db/persons} (:db qctx) - id (or (:person/id obj) (:cat/id obj))] - (->> (concat (vals cats) (vals persons)) - (filter (fn [a] - (contains? (or (:animal/loves a) #{}) id))) - vec) - ))} - {:d2q.field/name :find-all-humans - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [{:keys [db]} _ _ _] - (->> db :db/persons vals))} - {:d2q.field/name :find-all-cats - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [{:keys [db]} _ _ _] - (->> db :db/cats vals))} - {:d2q.field/name :find-persons-with-gender - :doc "Example of a cardinality-many entity-typed FR with params" - :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :bs.d2q.field/acl [:everyone-can-read] - :bs.d2q.field/compute - (fn [{:keys [db]} _ _ [gender]] - (->> db :db/persons vals - (filter #(-> % :person/gender (= gender)))))} - - (basic-fr :person/id false false "UID of a person") - (basic-fr :person/email false false "Email of a person") - (basic-fr :person/gender false false "Gender, an enum-valued FR, optional") - (basic-fr :person/age false false "Age, optional") - (basic-fr :person/address false false "Example of a map-valued scalar field") - (basic-fr :person/notes false false "Example of a list-valued scalar field") - (basic-fr :cat/id false false "") - (basic-fr :cat/name false false "") - (basic-fr :cat/owner true false "A to-one entity-typed field" - (fn [{:keys [db]} obj _ _] - (get-in db [:db/persons (:cat/owner obj)]))) - - (basic-fr :one-nil true false "A to-one FR returning nil" - (constantly nil)) - (basic-fr :many-nil true true "A to-many FR returning nil" - (constantly nil)) - (basic-fr :scalar-nil false false "A scalar FR returning nil" - (constantly nil)) - - (basic-fr :scalar-throws false false "" - (fn [_ _ _ _] (throw (ex-info "Scalar failed" {:error-data 42})))) - (basic-fr :one-throws true false "" - (fn [_ _ _ _] (throw (ex-info "To-one failed" {:error-data 42})))) - (basic-fr :many-throws true false "" - (fn [_ _ _ _] (throw (ex-info "To-many failed" {:error-data 42})))) - ]) - - (defn engine2 - [field-resolvers] - (d2q.api/query-engine - [(d2q.api/tabular-resolver-from-field-resolvers ::default field-resolvers)] - (map #(assoc % :d2q.field/resolver ::default) field-resolvers) - (fn [qctx query o+is] o+is))) - - - - (defn query-engine-example - [] - (fn [qctx query obj] - ;; Execution time mean : 228.790078 µs on first sample query - @((engine2 field-resolvers) - qctx (d2q/normalize-query query) obj)) - ;; Execution time mean : 48.560712 µs on first sample query - #_(d2q/engine field-resolvers - {:demand-driven.authorization.read/accesses-for-object (constantly #{:everyone-can-read})})) - - (defn q - "Runs a query on the example dataset" - [query] - (let [q-engine (query-engine-example) - qctx (qctx-example) - root-obj {}] - (q-engine qctx query root-obj))) - - (defn fcall - "Helper for writing Field Calls concisely" - ([field-name nested] - {:d2q.fcall/field field-name - :d2q.fcall/nested nested}) - ([field-name key args] - {:d2q.fcall/key key - :d2q.fcall/field field-name - :d2q.fcall/args args}) - ([field-name key args nested] - {:d2q.fcall/key key - :d2q.fcall/field field-name - :d2q.fcall/args args - :d2q.fcall/nested nested})) - - (fact "Canonical example" - (q (let [human-q [:person/id :person/email :person/age :person/address - (fcall :animal/loves - [:person/id :cat/id])]] - [(fcall :find-person-by-id "jd" ["john-doe"] - human-q) - (fcall :find-all-humans "humans" nil - human-q) - (fcall :find-all-cats "m" nil - [:cat/id :cat/name - {:d2q.fcall/field :cat/owner - :d2q.fcall/nested - [:person/email]}])])) - => - {"jd" {:person/id "john-doe", - :person/email "john.doe@gmail.com", - :person/age 18, - :person/address {:address/number "48", :address/street "rue de Rome", :address/city "Paris"}, - :animal/loves [{:person/id "alice-hacker"} {:cat/id "minou"}]}, - "humans" [{:person/id "john-doe", - :person/email "john.doe@gmail.com", - :person/age 18, - :person/address {:address/number "48", :address/street "rue de Rome", :address/city "Paris"}, - :animal/loves [{:person/id "alice-hacker"} {:cat/id "minou"}]} - {:person/id "alice-hacker", :person/email "alice.hacker@gmail.com", :animal/loves []} - {:person/id "bob-moran", - :person/email "bob.moran@gmail.com", - :person/address {:address/number "17", :address/street "rue de Mars", :address/city "Orléans"}, - :animal/loves []}], - "m" [{:cat/id "minou", :cat/name "Minou", :cat/owner {:person/email "john.doe@gmail.com"}} - {:cat/id "fuzzy-fur", :cat/name "Fuzzy Fur", :cat/owner {:person/email "bob.moran@gmail.com"}} - {:cat/id "wild-cat", :cat/name "Wild Cat"}]} - ) - - (fact "When entity does not exist, not added to the result" - (q [{:d2q.fcall/key "x" - :d2q.fcall/field :find-person-by-id - :d2q.fcall/args ["does not exist"] - :d2q.fcall/nested - [:person/id - {:d2q.fcall/field :animal/loves - :d2q.fcall/nested - [:person/id :cat/id]}]}]) - => {} - - (q [{:d2q.fcall/key "w" - :d2q.fcall/field :find-cat-by-id - :d2q.fcall/args ["wild-cat"] - :d2q.fcall/nested - [:cat/id - {:d2q.fcall/field :cat/owner - :d2q.fcall/nested - [:person/id]}]}]) - => {"w" {:cat/id "wild-cat"}} - ) - - (fact "When a Field Resolver returns nil, the key is not added to the result." - (q [:many-nil :one-nil :scalar-nil]) - => {} - - (fact "When an Entity-typed Field Resolver returns nil, the nested fields are not computed" - (q [(fcall :one-nil - [:scalar-throws :one-throws :many-throws])]) - => {})) - - (fact "When a Field Resolver throws, the whole query fails, with informative data about the error." - (tabular - (fact - (try - (q [(fcall ?field-name ?field-name "aaaaaargs")]) - :should-have-failed - (catch Throwable err - (ex-data err))) - => - (contains - {:q-field {:d2q.fcall/field ?field-name, - :d2q.fcall/key ?field-name - :d2q.fcall/args "aaaaaargs"}, - :d2q.error/type :d2q.error.type/field-resolver-failed-to-compute - :error-data 42}) - ) - ?field-name - :scalar-throws - :one-throws - :many-throws - ))) - - - - - - - - - - diff --git a/test/d2q/test/example/persons.clj b/test/d2q/test/example/persons.clj index cd6e317..9757846 100644 --- a/test/d2q/test/example/persons.clj +++ b/test/d2q/test/example/persons.clj @@ -45,7 +45,7 @@ ;; or ElasticSearch. ;; We will build a d2q Query Server which serves the following Fields, -;; which names should be most self-explanaroty: +;; which names should be most self-explanatory: #{:myapp.persons/person-of-id :myapp.person/id @@ -535,3 +535,4 @@ :d2q.resolver/compute #'resolve-person-parents} {:d2q.resolver/name :myapp.resolvers/person-children :d2q.resolver/compute #'resolve-person-children}]})) +