diff --git a/CHANGELOG.md b/CHANGELOG.md index eceecbc72..dd79e386c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/api/reading.md b/docs/api/reading.md index feb6d5e4b..6d12917f3 100644 --- a/docs/api/reading.md +++ b/docs/api/reading.md @@ -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 diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 6c15852ac..41494e6af 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -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 --------------------------------------------------------------- diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 9cd1138ed..89a607629 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -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 diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index 5c5c2c31f..6e369bddf 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -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") diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 8f6b5701b..73e31b506 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -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"