ability to order embeded items (closes #509)

This commit is contained in:
Ruslan Talpa
2016-05-21 20:52:13 +03:00
parent 2cb04c1d5c
commit 45d0f85b0d
6 changed files with 64 additions and 13 deletions
+1
View File
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Support "-" in column names - @ruslantalpa - Support "-" in column names - @ruslantalpa
- Support column/node renaming `alias:column` - @ruslantalpa - Support column/node renaming `alias:column` - @ruslantalpa
- Accept posts from HTML forms - @begriffs - Accept posts from HTML forms - @begriffs
- Ability to order embedded entities - @ruslantalpa
### Fixed ### Fixed
- Return 401 or 403 for access denied rather than 404 - @begriffs - Return 401 or 403 for access denied rather than 404 - @begriffs
+7 -1
View File
@@ -172,6 +172,12 @@ GET /people?order=age.nullsfirst
GET /people?order=age.desc.nullslast GET /people?order=age.desc.nullslast
``` ```
To filter the embedded items, you need to specify the tree path for the order param like so.
```HTTP
GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.ask
```
You can also use [computed You can also use [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS) columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
to order the results, even though the computed to order the results, even though the computed
@@ -285,7 +291,7 @@ GET /projects?id=eq.1&select=id, name, client{*}
Would embed in the `client` key the row referenced with `client_id`. Would embed in the `client` key the row referenced with `client_id`.
The `alias` feature works for embedded entities and also for regular columns. This is useful in situations where for example you use different naming conventions in the database and frontend. The `alias` feature works for embedded entities and also for regular columns. This is useful in situations where for example you use different naming conventions in the database and frontend.
The following request will produce the output below: The following request will produce the output below:
```HTTP ```HTTP
+7 -4
View File
@@ -77,8 +77,8 @@ data ApiRequest = ApiRequest {
, iFilters :: [(String, String)] , iFilters :: [(String, String)]
-- | &select parameter used to shape the response -- | &select parameter used to shape the response
, iSelect :: String , iSelect :: String
-- | &order parameter -- | &order parameters for each level
, iOrder :: Maybe String , iOrder :: [(String,String)]
-- | Alphabetized (canonical) request query string for response URLs -- | Alphabetized (canonical) request query string for response URLs
, iCanonicalQS :: String , iCanonicalQS :: String
-- | JSON Web Token -- | JSON Web Token
@@ -147,11 +147,11 @@ userApiRequest schema req reqBody =
, iPreferRepresentation = representation , iPreferRepresentation = representation
, iPreferSingular = singular , iPreferSingular = singular
, iPreferCount = not $ singular || hasPrefer "count=none" , iPreferCount = not $ singular || hasPrefer "count=none"
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ] , iFilters = [ (cs k, fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn "order" k) ]
, iSelect = if method == "DELETE" , iSelect = if method == "DELETE"
then "*" then "*"
else fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams else fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
, iOrder = join $ lookup "order" qParams , iOrder = [(cs k, fromJust v) | (k,v) <- qParams, isJust v, endingIn "order" k ]
, iCanonicalQS = urlEncodeVars , iCanonicalQS = urlEncodeVars
. sortBy (comparing fst) . sortBy (comparing fst)
. map (join (***) cs) . map (join (***) cs)
@@ -181,6 +181,9 @@ userApiRequest schema req reqBody =
tokenStr = case T.split (== ' ') (cs auth) of tokenStr = case T.split (== ' ') (cs auth) of
("Bearer" : t : _) -> t ("Bearer" : t : _) -> t
_ -> "" _ -> ""
endingIn:: T.Text -> T.Text -> Bool
endingIn word key = word == lastWord
where lastWord = last $ T.split (=='.') key
-- PRIVATE --------------------------------------------------------------- -- PRIVATE ---------------------------------------------------------------
+20 -8
View File
@@ -280,10 +280,10 @@ augumentRequestWithJoin schema allRels request =
buildReadRequest :: [Relation] -> ApiRequest -> Either Text ReadRequest buildReadRequest :: [Relation] -> ApiRequest -> Either Text ReadRequest
buildReadRequest allRels apiRequest = buildReadRequest allRels apiRequest =
augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> readRequest <*> ord) <*> flts) augumentRequestWithJoin schema rels =<<
first formatParserError (foldr addFilter <$> (foldr addOrder <$> readRequest <*> ords) <*> flts)
where where
selStr = iSelect apiRequest selStr = iSelect apiRequest
orderS = iOrder apiRequest
action = iAction apiRequest action = iAction apiRequest
target = iTarget apiRequest target = iTarget apiRequest
(schema, rootTableName) = fromJust $ -- Make it safe (schema, rootTableName) = fromJust $ -- Make it safe
@@ -303,9 +303,9 @@ buildReadRequest allRels apiRequest =
_ -> allRels _ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
readRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++selStr++">>") selStr readRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++selStr++">>") selStr
addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
flts = mapM pRequestFilter filters flts = mapM pRequestFilter filters
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS orders = iOrder apiRequest
ords = mapM pRequestOrder orders
buildMutateRequest :: ApiRequest -> Either Text MutateRequest buildMutateRequest :: ApiRequest -> Either Text MutateRequest
buildMutateRequest apiRequest = buildMutateRequest apiRequest =
@@ -326,12 +326,24 @@ buildMutateRequest apiRequest =
mutateFilters = filter (not . ( '.' `elem` ) . fst) $ iFilters apiRequest -- update/delete filters can be only on the root table mutateFilters = filter (not . ( '.' `elem` ) . fst) $ iFilters apiRequest -- update/delete filters can be only on the root table
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
addFilterToNode :: Filter -> ReadRequest -> ReadRequest
addFilterToNode flt (Node (q@Select {flt_=flts}, i) f) = Node (q {flt_=flt:flts}, i) f
addFilter :: (Path, Filter) -> ReadRequest -> ReadRequest addFilter :: (Path, Filter) -> ReadRequest -> ReadRequest
addFilter ([], flt) (Node (q@Select {flt_=flts}, i) forest) = Node (q {flt_=flt:flts}, i) forest addFilter = addProperty addFilterToNode
addFilter (path, flt) (Node rn forest) =
addOrderToNode :: [OrderTerm] -> ReadRequest -> ReadRequest
addOrderToNode o (Node (q,i) f) = Node (q{order=Just o}, i) f
addOrder :: (Path, [OrderTerm]) -> ReadRequest -> ReadRequest
addOrder = addProperty addOrderToNode
addProperty :: (a -> ReadRequest -> ReadRequest) -> (Path, a) -> ReadRequest -> ReadRequest
addProperty f ([], a) n = f a n
addProperty f (path, a) (Node rn forest) =
case targetNode of case targetNode of
Nothing -> Node rn forest -- the filter is silenty dropped in the Request does not contain the required path Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addFilter (remainingPath, flt) tn:restForest) Just tn -> Node rn (addProperty f (remainingPath, a) tn:restForest)
where where
targetNodeName:remainingPath = path targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest (targetNode,restForest) = splitForest targetNodeName forest
+7
View File
@@ -38,6 +38,13 @@ pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
op = fst <$> opVal op = fst <$> opVal
val = snd <$> opVal val = snd <$> opVal
pRequestOrder :: (String, String) -> Either ParseError (Path, [OrderTerm])
pRequestOrder (k, v) = (,) <$> path <*> ord
where
treePath = parse pTreePath ("failed to parser tree path (" ++ k ++ ")") k
path = fst <$> treePath
ord = parse pOrder ("failed to parse order (" ++ v ++ ")") v
ws :: Parser Text ws :: Parser Text
ws = cs <$> many (oneOf " \t") ws = cs <$> many (oneOf " \t")
+22
View File
@@ -362,6 +362,28 @@ spec = do
it "without other constraints" $ it "without other constraints" $
get "/items?order=id.asc" `shouldRespondWith` 200 get "/items?order=id.asc" `shouldRespondWith` 200
it "ordering embeded entities" $
get "/projects?id=eq.1&select=id, name, tasks{id, name}&tasks.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]
it "ordering embeded entities with alias" $
get "/projects?id=eq.1&select=id, name, the_tasks:tasks{id, name}&tasks.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]
it "ordering embeded entities, two levels" $
get "/projects?id=eq.1&select=id, name, tasks{id, name, users{id, name}}&tasks.order=name.asc&tasks.users.order=name.desc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7","users":[{"id":1,"name":"Angela Martin"}]},{"id":1,"name":"Design w7","users":[{"id":3,"name":"Dwight Schrute"},{"id":1,"name":"Angela Martin"}]}]}]|]
it "ordering embeded parents does not break things" $
get "/projects?id=eq.1&select=id, name, clients{id, name}&clients.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]
it "ordering embeded parents does not break things when using ducktape names" $
get "/projects?id=eq.1&select=id, name, client{id, name}&client.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}]|]
describe "Accept headers" $ do describe "Accept headers" $ do
it "should respond an unknown accept type with 415" $ it "should respond an unknown accept type with 415" $
request methodGet "/simple_pk" request methodGet "/simple_pk"