ability to order embeded items (closes #509)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user