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 column/node renaming `alias:column` - @ruslantalpa
- Accept posts from HTML forms - @begriffs
- Ability to order embedded entities - @ruslantalpa
### Fixed
- 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
```
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
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
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`.
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:
```HTTP
+7 -4
View File
@@ -77,8 +77,8 @@ data ApiRequest = ApiRequest {
, iFilters :: [(String, String)]
-- | &select parameter used to shape the response
, iSelect :: String
-- | &order parameter
, iOrder :: Maybe String
-- | &order parameters for each level
, iOrder :: [(String,String)]
-- | Alphabetized (canonical) request query string for response URLs
, iCanonicalQS :: String
-- | JSON Web Token
@@ -147,11 +147,11 @@ userApiRequest schema req reqBody =
, iPreferRepresentation = representation
, iPreferSingular = singular
, 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"
then "*"
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
. sortBy (comparing fst)
. map (join (***) cs)
@@ -181,6 +181,9 @@ userApiRequest schema req reqBody =
tokenStr = case T.split (== ' ') (cs auth) of
("Bearer" : t : _) -> t
_ -> ""
endingIn:: T.Text -> T.Text -> Bool
endingIn word key = word == lastWord
where lastWord = last $ T.split (=='.') key
-- PRIVATE ---------------------------------------------------------------
+20 -8
View File
@@ -280,10 +280,10 @@ augumentRequestWithJoin schema allRels request =
buildReadRequest :: [Relation] -> ApiRequest -> Either Text ReadRequest
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
selStr = iSelect apiRequest
orderS = iOrder apiRequest
action = iAction apiRequest
target = iTarget apiRequest
(schema, rootTableName) = fromJust $ -- Make it safe
@@ -303,9 +303,9 @@ buildReadRequest allRels apiRequest =
_ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
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
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 =
@@ -326,12 +326,24 @@ buildMutateRequest apiRequest =
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
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 ([], flt) (Node (q@Select {flt_=flts}, i) forest) = Node (q {flt_=flt:flts}, i) forest
addFilter (path, flt) (Node rn forest) =
addFilter = addProperty addFilterToNode
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
Nothing -> Node rn forest -- the filter is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addFilter (remainingPath, flt) tn:restForest)
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addProperty f (remainingPath, a) tn:restForest)
where
targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest
+7
View File
@@ -38,6 +38,13 @@ pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
op = fst <$> 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 = cs <$> many (oneOf " \t")
+22
View File
@@ -362,6 +362,28 @@ spec = do
it "without other constraints" $
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
it "should respond an unknown accept type with 415" $
request methodGet "/simple_pk"