CSV responses!
This commit is contained in:
+17
-6
@@ -62,11 +62,12 @@ app conf reqBody req =
|
|||||||
then return $ responseLBS status416 [] "HTTP Range error"
|
then return $ responseLBS status416 [] "HTTP Range error"
|
||||||
else do
|
else do
|
||||||
let qt = qualify table
|
let qt = qualify table
|
||||||
|
from = fromMaybe 0 $ rangeOffset <$> range
|
||||||
select = B.Stmt "select " V.empty True <>
|
select = B.Stmt "select " V.empty True <>
|
||||||
parentheticT (
|
parentheticT (
|
||||||
whereT qt qq $ countRows qt
|
whereT qt qq $ countRows qt
|
||||||
) <> commaq <> (
|
) <> commaq <> (
|
||||||
asJsonWithCount
|
bodyForAccept accept qt
|
||||||
. limitT range
|
. limitT range
|
||||||
. orderT (orderParse qq)
|
. orderT (orderParse qq)
|
||||||
. whereT qt qq
|
. whereT qt qq
|
||||||
@@ -75,7 +76,6 @@ app conf reqBody req =
|
|||||||
row <- H.maybeEx select
|
row <- H.maybeEx select
|
||||||
let (tableTotal, queryTotal, body) =
|
let (tableTotal, queryTotal, body) =
|
||||||
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
||||||
from = fromMaybe 0 $ rangeOffset <$> range
|
|
||||||
to = from+queryTotal-1
|
to = from+queryTotal-1
|
||||||
contentRange = contentRangeH from to tableTotal
|
contentRange = contentRangeH from to tableTotal
|
||||||
status = rangeStatus from to tableTotal
|
status = rangeStatus from to tableTotal
|
||||||
@@ -85,7 +85,7 @@ app conf reqBody req =
|
|||||||
. parseSimpleQuery
|
. parseSimpleQuery
|
||||||
$ rawQueryString req
|
$ rawQueryString req
|
||||||
return $ responseLBS status
|
return $ responseLBS status
|
||||||
[jsonH, contentRange,
|
[if accept == Just "text/csv" then csvH else jsonH, contentRange,
|
||||||
("Content-Location",
|
("Content-Location",
|
||||||
"/" <> cs table <>
|
"/" <> cs table <>
|
||||||
if Prelude.null canonical then "" else "?" <> cs canonical
|
if Prelude.null canonical then "" else "?" <> cs canonical
|
||||||
@@ -129,9 +129,9 @@ app conf reqBody req =
|
|||||||
|
|
||||||
([table], "POST") -> do
|
([table], "POST") -> do
|
||||||
let qt = qualify table
|
let qt = qualify table
|
||||||
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
echoRequested = lookupHeader "Prefer" == Just "return=representation"
|
||||||
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
|
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
|
||||||
parsed = if lookup "Content-Type" hdrs == Just "text/csv"
|
parsed = if lookupHeader "Content-Type" == Just "text/csv"
|
||||||
then do
|
then do
|
||||||
rows <- CSV.decode CSV.NoHeader reqBody
|
rows <- CSV.decode CSV.NoHeader reqBody
|
||||||
if V.null rows then Left "CSV requires header"
|
if V.null rows then Left "CSV requires header"
|
||||||
@@ -200,7 +200,7 @@ app conf reqBody req =
|
|||||||
let (queryTotal, body) =
|
let (queryTotal, body) =
|
||||||
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
|
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
|
||||||
r = contentRangeH 0 (queryTotal-1) queryTotal
|
r = contentRangeH 0 (queryTotal-1) queryTotal
|
||||||
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
echoRequested = lookupHeader "Prefer" == Just "return=representation"
|
||||||
s = case () of _ | queryTotal == 0 -> status404
|
s = case () of _ | queryTotal == 0 -> status404
|
||||||
| echoRequested -> status200
|
| echoRequested -> status200
|
||||||
| otherwise -> status204
|
| otherwise -> status204
|
||||||
@@ -232,6 +232,8 @@ app conf reqBody req =
|
|||||||
jwtSecret = cs $ configJwtSecret conf
|
jwtSecret = cs $ configJwtSecret conf
|
||||||
range = rangeRequested hdrs
|
range = rangeRequested hdrs
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
|
lookupHeader = flip lookup hdrs
|
||||||
|
accept = lookupHeader hAccept
|
||||||
|
|
||||||
sqlError :: t
|
sqlError :: t
|
||||||
sqlError = undefined
|
sqlError = undefined
|
||||||
@@ -245,6 +247,12 @@ rangeStatus from to total
|
|||||||
| (1 + to - from) < total = status206
|
| (1 + to - from) < total = status206
|
||||||
| otherwise = status200
|
| otherwise = status200
|
||||||
|
|
||||||
|
bodyForAccept :: Maybe BS.ByteString -> QualifiedTable -> StatementT
|
||||||
|
bodyForAccept accept table =
|
||||||
|
case accept of
|
||||||
|
Just "text/csv" -> asCsvWithCount table
|
||||||
|
_ -> asJsonWithCount -- defaults to JSON
|
||||||
|
|
||||||
contentRangeH :: Int -> Int -> Int -> Header
|
contentRangeH :: Int -> Int -> Int -> Header
|
||||||
contentRangeH from to total =
|
contentRangeH from to total =
|
||||||
("Content-Range",
|
("Content-Range",
|
||||||
@@ -268,6 +276,9 @@ requestedSchema v1schema hdrs =
|
|||||||
jsonH :: Header
|
jsonH :: Header
|
||||||
jsonH = (hContentType, "application/json")
|
jsonH = (hContentType, "application/json")
|
||||||
|
|
||||||
|
csvH :: Header
|
||||||
|
csvH = (hContentType, "text/csv")
|
||||||
|
|
||||||
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
|
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
|
||||||
-> H.Tx P.Postgres s Response
|
-> H.Tx P.Postgres s Response
|
||||||
handleJsonObj reqBody handler = do
|
handleJsonObj reqBody handler = do
|
||||||
|
|||||||
@@ -100,10 +100,27 @@ countT s =
|
|||||||
countRows :: QualifiedTable -> PStmt
|
countRows :: QualifiedTable -> PStmt
|
||||||
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQt t) empty True
|
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQt t) empty True
|
||||||
|
|
||||||
|
asCsvWithCount :: QualifiedTable -> StatementT
|
||||||
|
asCsvWithCount table = withCount . asCsv table
|
||||||
|
|
||||||
|
asCsv :: QualifiedTable -> StatementT
|
||||||
|
asCsv table s = s { B.stmtTemplate =
|
||||||
|
"(select string_agg(quote_ident(column_name::text), ',') from "
|
||||||
|
<> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '"
|
||||||
|
<> fromQt table <> "' order by ordinal_position) h) || '\r' || "
|
||||||
|
<> "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\r'), '') from ("
|
||||||
|
<> B.stmtTemplate s <> ") t" }
|
||||||
|
|
||||||
asJsonWithCount :: StatementT
|
asJsonWithCount :: StatementT
|
||||||
asJsonWithCount s = s { B.stmtTemplate =
|
asJsonWithCount = withCount . asJson
|
||||||
"pg_catalog.count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
|
|
||||||
<> B.stmtTemplate s <> ") t" }
|
asJson :: StatementT
|
||||||
|
asJson s = s { B.stmtTemplate =
|
||||||
|
"array_to_json(array_agg(row_to_json(t)))::character varying from ("
|
||||||
|
<> B.stmtTemplate s <> ") t" }
|
||||||
|
|
||||||
|
withCount :: StatementT
|
||||||
|
withCount s = s { B.stmtTemplate = "pg_catalog.count(t), " <> B.stmtTemplate s }
|
||||||
|
|
||||||
asJsonRow :: StatementT
|
asJsonRow :: StatementT
|
||||||
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
|
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module Feature.QuerySpec where
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.HTTP.Types
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -121,6 +122,16 @@ spec =
|
|||||||
it "without other constraints" $
|
it "without other constraints" $
|
||||||
get "/items?order=asc.id" `shouldRespondWith` 200
|
get "/items?order=asc.id" `shouldRespondWith` 200
|
||||||
|
|
||||||
|
describe "Accept headers" $
|
||||||
|
it "should respond with CSV to 'text/csv' request" $
|
||||||
|
request methodGet "/simple_pk"
|
||||||
|
(acceptHdrs "text/csv") ""
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just "k,extra\rxyyx,u\rxYYx,v"
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "text/csv"]
|
||||||
|
}
|
||||||
|
|
||||||
describe "Canonical location" $ do
|
describe "Canonical location" $ do
|
||||||
it "Sets Content-Location with alphabetized params" $
|
it "Sets Content-Location with alphabetized params" $
|
||||||
get "/no_pk?b=eq.1&a=eq.1"
|
get "/no_pk?b=eq.1&a=eq.1"
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,7 @@ import qualified Data.Vector as V
|
|||||||
import Control.Monad (void)
|
import Control.Monad (void)
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
hRange, hAuthorization)
|
hRange, hAuthorization, hAccept)
|
||||||
import Codec.Binary.Base64.String (encode)
|
import Codec.Binary.Base64.String (encode)
|
||||||
import Data.CaseInsensitive (CI(..))
|
import Data.CaseInsensitive (CI(..))
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
@@ -84,6 +84,9 @@ loadFixture name =
|
|||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||||
|
|
||||||
|
acceptHdrs :: BS.ByteString -> [Header]
|
||||||
|
acceptHdrs mime = [(hAccept, mime)]
|
||||||
|
|
||||||
rangeUnit :: Header
|
rangeUnit :: Header
|
||||||
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user