cognitect-labs / vase

Data driven microservices
Eclipse Public License 1.0
374 stars 40 forks source link

Scalar/non-sequential query results yield errors #49

Closed milt closed 7 years ago

milt commented 7 years ago

Description

When I use the #vase/query literal to perform a query without any trailing interceptors, queries with a non sequential find-spec (like a single scalar value or a pulled map) generate an error. Queries with nil results also generate a spec error, but don't throw.

Expected Behavior

When I write a query like so:

"/users/:id" {:get #vase/query {:name :query-result-error.v1/user-id-page
                                     :params [id]
                                     :edn-coerce [id]
                                     :query [:find ?e .
                                             :in $ ?id
                                             :where
                                             [?e :user/userId ?id]]}}

I should get a result that is just the user's eid.

Actual Behavior

If the user exists, It throws:

HTTP/1.1 500 Server Error
Connection: close
Date: Wed, 08 Mar 2017 13:48:41 GMT
Content-Type: text/plain

Error processing request!
Exception:

clojure.lang.ExceptionInfo: Interceptor Exception: Don't know how to create ISeq from: java.lang.Long
 at clojure.core$ex_info.invokeStatic (core.clj:4725)
    clojure.core$ex_info.invoke (core.clj:4725)
    io.pedestal.interceptor.chain$throwable__GT_ex_info.invokeStatic (chain.clj:33)
    io.pedestal.interceptor.chain$throwable__GT_ex_info.invoke (chain.clj:32)
    io.pedestal.interceptor.chain$try_f.invokeStatic (chain.clj:55)
    io.pedestal.interceptor.chain$try_f.invoke (chain.clj:42)
    io.pedestal.interceptor.chain$process_all_with_binding.invokeStatic (chain.clj:169)
    io.pedestal.interceptor.chain$process_all_with_binding.invoke (chain.clj:144)
    io.pedestal.interceptor.chain$process_all$fn__8778.invoke (chain.clj:186)
    clojure.lang.AFn.applyToHelper (AFn.java:152)
    clojure.lang.AFn.applyTo (AFn.java:144)
    clojure.core$apply.invokeStatic (core.clj:657)
    clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1963)
    clojure.core$with_bindings_STAR_.doInvoke (core.clj:1963)
    clojure.lang.RestFn.invoke (RestFn.java:425)
    io.pedestal.interceptor.chain$process_all.invokeStatic (chain.clj:184)
    io.pedestal.interceptor.chain$process_all.invoke (chain.clj:180)
    io.pedestal.interceptor.chain$enter_all.invokeStatic (chain.clj:233)
    io.pedestal.interceptor.chain$enter_all.invoke (chain.clj:227)
    io.pedestal.interceptor.chain$execute.invokeStatic (chain.clj:377)
    io.pedestal.interceptor.chain$execute.invoke (chain.clj:350)
    io.pedestal.interceptor.chain$execute.invokeStatic (chain.clj:387)
    io.pedestal.interceptor.chain$execute.invoke (chain.clj:350)
    io.pedestal.http.impl.servlet_interceptor$interceptor_service_fn$fn__12774.invoke (servlet_interceptor.clj:350)
    io.pedestal.http.servlet.FnServlet.service (servlet.clj:28)
    org.eclipse.jetty.servlet.ServletHolder.handle (ServletHolder.java:838)
    org.eclipse.jetty.servlet.ServletHandler.doHandle (ServletHandler.java:543)
    org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:188)
    org.eclipse.jetty.server.handler.ContextHandler.doHandle (ContextHandler.java:1228)
    org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:168)
    org.eclipse.jetty.servlet.ServletHandler.doScope (ServletHandler.java:481)
    org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:166)
    org.eclipse.jetty.server.handler.ContextHandler.doScope (ContextHandler.java:1130)
    org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:141)
    org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:132)
    org.eclipse.jetty.server.Server.handle (Server.java:564)
    org.eclipse.jetty.server.HttpChannel.handle (HttpChannel.java:318)
    org.eclipse.jetty.server.HttpConnection.onFillable (HttpConnection.java:251)
    org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded (AbstractConnection.java:279)
    org.eclipse.jetty.io.FillInterest.fillable (FillInterest.java:112)
    org.eclipse.jetty.io.ChannelEndPoint$2.run (ChannelEndPoint.java:124)
    org.eclipse.jetty.util.thread.QueuedThreadPool.runJob (QueuedThreadPool.java:672)
    org.eclipse.jetty.util.thread.QueuedThreadPool$2.run (QueuedThreadPool.java:590)
    java.lang.Thread.run (Thread.java:745)
Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Long
 at clojure.lang.RT.seqFrom (RT.java:547)
    clojure.lang.RT.seq (RT.java:527)
    clojure.core$seq__6422.invokeStatic (core.clj:137)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:24)
    clojure.core.protocols$fn__9120.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__9072$G__9067__9085.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6704)
    clojure.core$into.invokeStatic (core.clj:6771)
    clojure.core$into.invoke (core.clj:6763)
    clojure.core$eval27928$fn__27930.invoke (NO_SOURCE_FILE:0)
    io.pedestal.interceptor.chain$try_f.invokeStatic (chain.clj:52)
    io.pedestal.interceptor.chain$try_f.invoke (chain.clj:42)
    io.pedestal.interceptor.chain$process_all_with_binding.invokeStatic (chain.clj:169)
    io.pedestal.interceptor.chain$process_all_with_binding.invoke (chain.clj:144)
    io.pedestal.interceptor.chain$process_all$fn__8778.invoke (chain.clj:186)
    clojure.lang.AFn.applyToHelper (AFn.java:152)
    clojure.lang.AFn.applyTo (AFn.java:144)
    clojure.core$apply.invokeStatic (core.clj:657)
    clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1963)
    clojure.core$with_bindings_STAR_.doInvoke (core.clj:1963)
    clojure.lang.RestFn.invoke (RestFn.java:425)
    io.pedestal.interceptor.chain$process_all.invokeStatic (chain.clj:184)
    io.pedestal.interceptor.chain$process_all.invoke (chain.clj:180)
    io.pedestal.interceptor.chain$enter_all.invokeStatic (chain.clj:233)
    io.pedestal.interceptor.chain$enter_all.invoke (chain.clj:227)
    io.pedestal.interceptor.chain$execute.invokeStatic (chain.clj:377)
    io.pedestal.interceptor.chain$execute.invoke (chain.clj:350)
    io.pedestal.interceptor.chain$execute.invokeStatic (chain.clj:387)
    io.pedestal.interceptor.chain$execute.invoke (chain.clj:350)
    io.pedestal.http.impl.servlet_interceptor$interceptor_service_fn$fn__12774.invoke (servlet_interceptor.clj:350)
    io.pedestal.http.servlet.FnServlet.service (servlet.clj:28)
    org.eclipse.jetty.servlet.ServletHolder.handle (ServletHolder.java:838)
    org.eclipse.jetty.servlet.ServletHandler.doHandle (ServletHandler.java:543)
    org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:188)
    org.eclipse.jetty.server.handler.ContextHandler.doHandle (ContextHandler.java:1228)
    org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:168)
    org.eclipse.jetty.servlet.ServletHandler.doScope (ServletHandler.java:481)
    org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:166)
    org.eclipse.jetty.server.handler.ContextHandler.doScope (ContextHandler.java:1130)
    org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:141)
    org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:132)
    org.eclipse.jetty.server.Server.handle (Server.java:564)
    org.eclipse.jetty.server.HttpChannel.handle (HttpChannel.java:318)
    org.eclipse.jetty.server.HttpConnection.onFillable (HttpConnection.java:251)
    org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded (AbstractConnection.java:279)
    org.eclipse.jetty.io.FillInterest.fillable (FillInterest.java:112)
    org.eclipse.jetty.io.ChannelEndPoint$2.run (ChannelEndPoint.java:124)
    org.eclipse.jetty.util.thread.QueuedThreadPool.runJob (QueuedThreadPool.java:672)
    org.eclipse.jetty.util.thread.QueuedThreadPool$2.run (QueuedThreadPool.java:590)
    java.lang.Thread.run (Thread.java:745)

Context:

{:response nil,
 :cors-headers
 {"Access-Control-Allow-Origin" "",
  "Access-Control-Allow-Credentials" "true"},
 :io.pedestal.interceptor.chain/stack
 ({:name :io.pedestal.http.impl.servlet-interceptor/ring-response,
   :enter nil,
   :leave
   #function[io.pedestal.http.impl.servlet-interceptor/leave-ring-response],
   :error
   #function[io.pedestal.http.impl.servlet-interceptor/error-ring-response]}
  {:name :io.pedestal.http.impl.servlet-interceptor/stylobate,
   :enter
   #function[io.pedestal.http.impl.servlet-interceptor/enter-stylobate],
   :leave
   #function[io.pedestal.http.impl.servlet-interceptor/leave-stylobate],
   :error
   #function[io.pedestal.http.impl.servlet-interceptor/error-stylobate]}
  {:name
   :io.pedestal.http.impl.servlet-interceptor/terminator-injector,
   :enter #function[io.pedestal.interceptor.helpers/before/fn--8985],
   :leave nil,
   :error nil}),
 :request
 {:request-id "T1UaC1shBGI",
  :protocol "HTTP/1.1",
  :db datomic.db.Db@cc2ea76c,
  :async-supported? true,
  :remote-addr "127.0.0.1",
  :servlet-response
  #object[org.eclipse.jetty.server.Response 0x29523707 "HTTP/1.1 200 \nConnection: close\r\nDate: Wed, 08 Mar 2017 13:48:41 GMT\r\n\r\n"],
  :servlet
  #object[io.pedestal.http.servlet.FnServlet 0x122db885 "io.pedestal.http.servlet.FnServlet@122db885"],
  :headers
  {"connection" "close",
   "user-agent" "Paw/3.0.16 (Macintosh; OS X/10.12.3) GCDHTTPRequest",
   "host" "localhost:8080",
   "origin" "",
   "vaserequest-id" "T1UaC1shBGI"},
  :server-port 8080,
  :servlet-request
  #object[org.eclipse.jetty.server.Request 0x1d76ebe2 "Request(GET //localhost:8080/api/query-result-error/v1/users/1)@1d76ebe2"],
  :path-info "/api/query-result-error/v1/users/1",
  :received-time
  #object[org.joda.time.DateTime 0x2cb041d0 "2017-03-08T13:48:41.238Z"],
  :url-for #function[io.pedestal.http.route/url-for-routes/fn--9801],
  :uri "/api/query-result-error/v1/users/1",
  :server-name "localhost",
  :query-string nil,
  :path-params {:id "1"},
  :body
  #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x22107cb2 "HttpInputOverHTTP@22107cb2[c=0,q=0,[0]=null,s=STREAM]"],
  :scheme :http,
  :request-method :get,
  :conn
  #object[datomic.peer.LocalConnection 0x2dce6a20 "datomic.peer.LocalConnection@2dce6a20"]},
 :bindings
 {#'io.pedestal.http.route/*url-for*
  #function[io.pedestal.http.route/url-for-routes/fn--9801]},
 :enter-async
 [#function[io.pedestal.http.impl.servlet-interceptor/start-servlet-async]],
 :io.pedestal.interceptor.chain/terminators
 (#function[io.pedestal.http.impl.servlet-interceptor/terminator-inject/fn--12749]),
 :servlet-response
 #object[org.eclipse.jetty.server.Response 0x29523707 "HTTP/1.1 200 \nConnection: close\r\nDate: Wed, 08 Mar 2017 13:48:41 GMT\r\n\r\n"],
 :route
 {:path "/api/query-result-error/v1/users/:id",
  :method :get,
  :path-constraints {:id "([^/]+)"},
  :io.pedestal.http.route.prefix-tree/satisfies-constraints?
  #function[io.pedestal.http.route.prefix-tree/add-satisfies-constraints?/fn--9676],
  :path-re #"/\Qapi\E/\Qquery-result-error\E/\Qv1\E/\Qusers\E/([^/]+)",
  :path-parts ["api" "query-result-error" "v1" "users" :id],
  :interceptors
  [{:name :attach-received-time,
    :enter #function[com.cognitect.vase.interceptor/fn--14423],
    :leave nil,
    :error nil,
    :doc "Attaches a timestamp to every request."}
   {:name :attach-request-id,
    :enter #function[com.cognitect.vase.interceptor/fn--14426],
    :leave nil,
    :error nil,
    :doc
    "Attaches a request ID to every request;\n            If there's a 'request_id' header, it will use that value, otherwise it will generate a short hash"}
   {:name :io.pedestal.http/json-body,
    :enter nil,
    :leave
    #function[io.pedestal.interceptor.helpers/on-response/fn--9062],
    :error nil}
   {:name :com.cognitect.vase.datomic/insert-datomic,
    :enter
    #function[com.cognitect.vase.datomic/insert-datomic/fn--13969],
    :leave nil,
    :error nil}
   {:name :io.pedestal.http.body-params/body-params,
    :enter
    #function[io.pedestal.interceptor.helpers/on-request/fn--9041],
    :leave nil,
    :error nil}
   {:name :forward-headers,
    :enter nil,
    :leave
    #function[com.cognitect.vase.interceptor/forward-headers/fn--14430],
    :error nil,
    :doc
    "Given an interceptor name and list of headers to forward,\n            return an interceptor that attaches those headers to reponses IFF\n            they are in the request"}
   {:name :query-result-error.v1/user-id-page,
    :enter #function[clojure.core/eval27928/fn--27930],
    :leave nil,
    :error nil,
    :action-literal :vase/query}],
  :route-name :query-result-error.v1/user-id-page,
  :path-params {:id "1"}},
 :servlet
 #object[io.pedestal.http.servlet.FnServlet 0x122db885 "io.pedestal.http.servlet.FnServlet@122db885"],
 :servlet-request
 #object[org.eclipse.jetty.server.Request 0x1d76ebe2 "Request(GET //localhost:8080/api/query-result-error/v1/users/1)@1d76ebe2"],
 :url-for #function[io.pedestal.http.route/url-for-routes/fn--9801],
 :io.pedestal.interceptor.chain/execution-id 12,
 :servlet-config
 #object[org.eclipse.jetty.servlet.ServletHolder$Config 0x45d22a43 "org.eclipse.jetty.servlet.ServletHolder$Config@45d22a43"],
 :async?
 #function[io.pedestal.http.impl.servlet-interceptor/servlet-async?]}

If the user doesn't exist, I get:

HTTP/1.1 400 Bad Request
Connection: close
Date: Wed, 08 Mar 2017 13:55:05 GMT
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Download-Options: noopen
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Permitted-Cross-Domain-Policies: none
vaserequest-id: FF8LMCphCQM
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, X-Xss-Protection, X-Download-Options, X-Permitted-Cross-Domain-Policies, Content-Security-Policy, Vaserequest-Id
X-Content-Type-Options: nosniff
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain

Missing required query parameters; One or more parameters was `nil`.  Got: (:id)  Required: [:id]

Steps to reproduce

  1. Use the vase template to make a fresh project with the example svc file.
  2. Add a user via PUT
  3. Modify the /user/:id GET route query to return a scalar result (add .)
  4. Try to GET /user/:id (5). Query /user/:id for a user that doesn't exist (to see the spec error)

I made an example repo with the template + modifications here

Environment

Operating System (including version).

Mac OS X 10.12.3

Your current Leiningen/Boot/Maven version (lein --version)

Leiningen 2.7.1 on Java 1.8.0_111 Java HotSpot(TM) 64-Bit Server VM

Pedestal and Vase version

0.5.2/0.9.0

mtnygard commented 7 years ago

Thank you for this very high quality bug report!

milt commented 7 years ago

You're welcome, but be careful what you wish for... Our team is an endless source of datomic edge cases. ;)

mtnygard commented 7 years ago

The fix for this issue created a new problem. Prior to commit 831336998, a query using pull with a relation binding (see below) returned a list of maps. After that commit, it returns a list of list of maps.

Example:

{:get #vase/query 
  {:name :control-server.v1/controls-state
     :query [:find [(pull ?control [*
                              {:control/desired-state [:db/ident]}
                              {:control/actual-state  [:db/ident]}]) ...]
                 :in $
                 :where [?control :control/name]]}}