Merge pull request #375 from calebmer/feature/plurality-singular
Implement single route selection
This commit is contained in:
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Full text operators `@>`,`<@` - @ruslantalpa
|
- Full text operators `@>`,`<@` - @ruslantalpa
|
||||||
- Shaping of the response body (filter columns, embed relations) with &select parameter for POST/PATCH - @ruslantalpa
|
- Shaping of the response body (filter columns, embed relations) with &select parameter for POST/PATCH - @ruslantalpa
|
||||||
- Detect relationships between public views and private tables - @calebmer
|
- Detect relationships between public views and private tables - @calebmer
|
||||||
|
- `Prefer: plurality=singular` for selecting single objects - @calebmer
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- API versioning feature - @calebmer
|
- API versioning feature - @calebmer
|
||||||
|
|||||||
+30
-22
@@ -19,7 +19,7 @@ import qualified Data.HashMap.Strict as HM
|
|||||||
import Data.List (find, sortBy, delete, transpose)
|
import Data.List (find, sortBy, delete, transpose)
|
||||||
import Data.Maybe (fromMaybe, fromJust, isJust, isNothing, mapMaybe)
|
import Data.Maybe (fromMaybe, fromJust, isJust, isNothing, mapMaybe)
|
||||||
import Data.Ord (comparing)
|
import Data.Ord (comparing)
|
||||||
import Data.Ranged.Ranges (emptyRange)
|
import Data.Ranged.Ranges (emptyRange, singletonRange)
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Text (Text, replace, strip)
|
import Data.Text (Text, replace, strip)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
@@ -90,25 +90,30 @@ app dbStructure conf reqBody req =
|
|||||||
if range == Just emptyRange
|
if range == Just emptyRange
|
||||||
then return $ errResponse status416 "HTTP Range error"
|
then return $ errResponse status416 "HTTP Range error"
|
||||||
else do
|
else do
|
||||||
let q = createReadStatement selectQuery range (not $ hasPrefer "count=none") isCsv
|
let q = createReadStatement selectQuery (if singular then Nothing else range) singular (not $ hasPrefer "count=none") isCsv
|
||||||
row <- H.maybeEx q
|
row <- H.maybeEx q
|
||||||
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
|
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
|
||||||
frm = fromMaybe 0 $ rangeOffset <$> range
|
if singular
|
||||||
to = frm+queryTotal-1
|
then return $ if queryTotal <= 0
|
||||||
contentRange = contentRangeH frm to tableTotal
|
then responseLBS status404 [] ""
|
||||||
status = rangeStatus frm to tableTotal
|
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
|
||||||
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
else do
|
||||||
. sortBy (comparing fst)
|
let frm = fromMaybe 0 $ rangeOffset <$> range
|
||||||
. map (join (***) cs)
|
to = frm+queryTotal-1
|
||||||
. parseSimpleQuery
|
contentRange = contentRangeH frm to tableTotal
|
||||||
$ rawQueryString req
|
status = rangeStatus frm to tableTotal
|
||||||
return $ responseLBS status
|
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
||||||
[contentTypeH, contentRange,
|
. sortBy (comparing fst)
|
||||||
("Content-Location",
|
. map (join (***) cs)
|
||||||
"/" <> cs table <>
|
. parseSimpleQuery
|
||||||
if Prelude.null canonical then "" else "?" <> cs canonical
|
$ rawQueryString req
|
||||||
)
|
return $ responseLBS status
|
||||||
] (fromMaybe "[]" body)
|
[contentTypeH, contentRange,
|
||||||
|
("Content-Location",
|
||||||
|
"/" <> cs table <>
|
||||||
|
if Prelude.null canonical then "" else "?" <> cs canonical
|
||||||
|
)
|
||||||
|
] (fromMaybe "[]" body)
|
||||||
Right (selectQuery, Just (mutateQuery, isSingle)) ->
|
Right (selectQuery, Just (mutateQuery, isSingle)) ->
|
||||||
case verb of
|
case verb of
|
||||||
"POST" -> do
|
"POST" -> do
|
||||||
@@ -186,6 +191,7 @@ app dbStructure conf reqBody req =
|
|||||||
isCsv = contentType == csvMT
|
isCsv = contentType == csvMT
|
||||||
contentTypeH = (hContentType, contentType)
|
contentTypeH = (hContentType, contentType)
|
||||||
echoRequested = hasPrefer "return=representation"
|
echoRequested = hasPrefer "return=representation"
|
||||||
|
singular = hasPrefer "plurality=singular"
|
||||||
request = parseRequest schema (dbRelations dbStructure) (head path) req reqBody --TODO! is head safe?
|
request = parseRequest schema (dbRelations dbStructure) (head path) req reqBody --TODO! is head safe?
|
||||||
|
|
||||||
rangeStatus :: Int -> Int -> Maybe Int -> Status
|
rangeStatus :: Int -> Int -> Maybe Int -> Status
|
||||||
@@ -415,15 +421,17 @@ parseRequest schema allRels rootTableName httpRequest reqBody =
|
|||||||
selectQuery = requestToQuery schema <$> selectApiRequest
|
selectQuery = requestToQuery schema <$> selectApiRequest
|
||||||
mutateQuery = requestToQuery schema <$> mutateApiRequest
|
mutateQuery = requestToQuery schema <$> mutateApiRequest
|
||||||
|
|
||||||
createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> B.Stmt P.Postgres
|
createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
|
||||||
createReadStatement selectQuery range countTable asCsv =
|
createReadStatement selectQuery range isSingle countTable asCsv =
|
||||||
B.Stmt (
|
B.Stmt (
|
||||||
wrapQuery selectQuery [
|
wrapQuery selectQuery [
|
||||||
if countTable then countAllF else countNoneF,
|
if countTable then countAllF else countNoneF,
|
||||||
countF,
|
countF,
|
||||||
"null", -- location header can not be calucalted
|
"null", -- location header can not be calucalted
|
||||||
if asCsv then asCsvF else asJsonF
|
if asCsv
|
||||||
] selectStarF range
|
then asCsvF
|
||||||
|
else if isSingle then asJsonSingleF else asJsonF
|
||||||
|
] selectStarF (if isNothing range && isSingle then Just $ singletonRange 0 else range)
|
||||||
) V.empty True
|
) V.empty True
|
||||||
|
|
||||||
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> [Text] -> Bool -> B.Stmt P.Postgres
|
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> [Text] -> Bool -> B.Stmt P.Postgres
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ spec =
|
|||||||
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)" `shouldRespondWith`
|
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)" `shouldRespondWith`
|
||||||
"[{\"user_id\":2,\"task_id\":6,\"comments\":[{\"content\":\"Needs to be delivered ASAP\"}]}]"
|
"[{\"user_id\":2,\"task_id\":6,\"comments\":[{\"content\":\"Needs to be delivered ASAP\"}]}]"
|
||||||
|
|
||||||
|
describe "Plurality singular" $ do
|
||||||
|
it "will select an existing object" $
|
||||||
|
request methodGet "/items?id=eq.5" [("Prefer","plurality=singular")] ""
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| {"id":5} |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "will respond with 404 when not found" $
|
||||||
|
request methodGet "/items?id=eq.9999" [("Prefer","plurality=singular")] ""
|
||||||
|
`shouldRespondWith` 404
|
||||||
|
|
||||||
|
it "can shape plurality singular object routes" $
|
||||||
|
request methodGet "/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)" [("Prefer","plurality=singular")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
"{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}"
|
||||||
|
|
||||||
|
|
||||||
describe "ordering response" $ do
|
describe "ordering response" $ do
|
||||||
it "by a column asc" $
|
it "by a column asc" $
|
||||||
|
|||||||
Reference in New Issue
Block a user