From abe87f2f16047750e24f53a38c9501d9460de15b Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 15:28:15 -0500 Subject: [PATCH 1/8] Add support for selecting by column --- src/PostgREST/QueryBuilder.hs | 41 ++++++++++++++++++++--------------- src/PostgREST/Types.hs | 8 +++---- test/Feature/QuerySpec.hs | 6 ++++- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index cd3ba5036..b7c84e49f 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -120,20 +120,25 @@ createWriteStatement qi selectQuery mutateQuery isSingle Full | otherwise = asJsonF addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest -addRelations schema allRelations parentNode node@(Node n@(query, (table, _)) forest) = +addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _)) forest) = case parentNode of - Nothing -> Node (query, (table, Nothing)) <$> updatedForest - (Just (Node (_, (parentTable, _)) _)) -> Node <$> (addRel n <$> rel) <*> updatedForest + (Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest where - rel = note ("no relation between " <> table <> " and " <> parentTable) - $ findRelation schema table parentTable - <|> findRelation schema parentTable table + rel = note ("no relation between " <> parentTable <> " and " <> name) + $ findRelationTable schema name parentTable + <|> findRelationTable schema parentTable name + <|> findRelationColumn schema parentTable name addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation)) - addRel (q, (t, _)) r = (q, (t, Just r)) + addRel (q, (n, _)) r = (q {from=fromRelation}, (n, Just r)) + where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from q) + + _ -> Node (query, (name, Nothing)) <$> updatedForest where updatedForest = mapM (addRelations schema allRelations (Just node)) forest - findRelation s t1 t2 = - find (\r -> s == (tableSchema . relTable) r && t1 == (tableName . relTable) r && t2 == (tableName . relFTable) r) allRelations + findRelationTable s t1 t2 = + find (\r -> s == tableSchema (relTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations + findRelationColumn s t c = + find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c == (colName . head . relFColumns) r) allRelations addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest addJoinConditions schema (Node (query, (n, r)) forest) = @@ -218,11 +223,12 @@ requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) requestToQuery :: Schema -> DbRequest -> SqlQuery requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined -requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (mainTbl, _)) forest)) = +requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nodeName, maybeRelation)) forest)) = query where -- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name -- of our WITH query part + mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation) tblSchema tbl = if tbl == sourceCTEName then "" else schema qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl toQi t = QualifiedIdentifier (tblSchema t) t @@ -253,28 +259,27 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (mai intercalate " AND " ( map (pgFmtCondition qi ) joinConditions ) where joinConditions = filter (filterParentConditions t) conditions - filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = - parentTable == t + filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t filterParentConditions _ _ = False getQueryParts :: Tree ReadNode -> ([(SqlFragment, TableName)], [SqlFragment]) -> ([(SqlFragment,TableName)], [SqlFragment]) - getQueryParts (Node n@(_, (table, Just (Relation {relType=Child}))) forst) (j,s) = (j,sel:s) + getQueryParts (Node n@(_, (name, Just (Relation {relType=Child,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s) where sel = "COALESCE((" <> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) " <> "FROM (" <> subquery <> ") " <> table - <> "), '[]') AS " <> table + <> "), '[]') AS " <> pgFmtIdent name where subquery = requestToQuery schema (DbRead (Node n forst)) - getQueryParts (Node n@(_, (table, Just (Relation {relType=Parent}))) forst) (j,s) = (joi:j,sel:s) + getQueryParts (Node n@(_, (name, Just (Relation {relType=Parent,relTable=Table{tableName=table}}))) forst) (j,s) = (joi:j,sel:s) where - sel = "row_to_json(" <> table <> ".*) AS "<>table --TODO must be singular + sel = "row_to_json(" <> table <> ".*) AS "<>pgFmtIdent name --TODO must be singular joi = ("( " <> subquery <> " ) AS " <> table, table) where subquery = requestToQuery schema (DbRead (Node n forst)) - getQueryParts (Node n@(_, (table, Just (Relation {relType=Many}))) forst) (j,s) = (j,sel:s) + getQueryParts (Node n@(_, (name, Just (Relation {relType=Many,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s) where sel = "COALESCE ((" <> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) " <> "FROM (" <> subquery <> ") " <> table - <> "), '[]') AS " <> table + <> "), '[]') AS " <> pgFmtIdent name where subquery = requestToQuery schema (DbRead (Node n forst)) --the following is just to remove the warning --getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 2737ca386..8bd6b91c8 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -106,10 +106,10 @@ type Cast = Text type NodeName = Text type SelectItem = (Field, Maybe Cast) type Path = [Text] -data ReadQuery = Select { select::[SelectItem], from::[Text], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq) -data MutateQuery = Insert { in_::Text, qPayload::Payload } - | Delete { in_::Text, where_::[Filter] } - | Update { in_::Text, qPayload::Payload, where_::[Filter] } deriving (Show, Eq) +data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq) +data MutateQuery = Insert { in_::TableName, qPayload::Payload } + | Delete { in_::TableName, where_::[Filter] } + | Update { in_::TableName, qPayload::Payload, where_::[Filter] } deriving (Show, Eq) data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq) type ReadNode = (ReadQuery, (NodeName, Maybe Relation)) type ReadRequest = Tree ReadNode diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index aadc8edaf..1be7b7103 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -224,7 +224,11 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $ get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith` - [str|[{"id":1,"articlestars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|] + [str|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|] + + it "can select by column name" $ + get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith` + "[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client_id\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client_id\":{\"id\":2,\"name\":\"Apple\"}}]" describe "Plurality singular" $ do From 88d4798d5e80c98f0b7a2101cf16bbf65cba7196 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 16:15:33 -0500 Subject: [PATCH 2/8] Add column select sans id support --- src/PostgREST/QueryBuilder.hs | 6 ++++-- test/Feature/QuerySpec.hs | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index b7c84e49f..0bd7e8666 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE TupleSections #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-| Module : PostgREST.QueryBuilder @@ -138,7 +139,8 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _ findRelationTable s t1 t2 = find (\r -> s == tableSchema (relTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations findRelationColumn s t c = - find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c == (colName . head . relFColumns) r) allRelations + find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` (colName . head . relFColumns) r) allRelations + where n `colMatches` rc = (cs ("^" <> rc <> "(?:|_id|Id)$") :: BS.ByteString) =~ (cs n :: BS.ByteString) addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest addJoinConditions schema (Node (query, (n, r)) forest) = diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 1be7b7103..dee897709 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -230,6 +230,10 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith` "[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client_id\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client_id\":{\"id\":2,\"name\":\"Apple\"}}]" + it "can select by column name sans id" $ + get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith` + "[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client\":{\"id\":2,\"name\":\"Apple\"}}]" + describe "Plurality singular" $ do it "will select an existing object" $ From 6446fc962d4a27c8286df2b6f0211a7625ed43b2 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 18:09:29 -0500 Subject: [PATCH 3/8] Add changelog note --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a51eb06f..072c255af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Allow order by computed columns - @diogob - Set max rows in response with --max-rows - @begriffs +- Selection by column name (can detect if `_id` is not included) - @calebmer ## [0.3.0.1] - 2015-11-27 From ee82ad1864821f4bc3f0044863d6e19386d503dd Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 18:10:55 -0500 Subject: [PATCH 4/8] Better documentation adding relations --- src/PostgREST/QueryBuilder.hs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 0bd7e8666..ec3407b58 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -126,9 +126,9 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _ (Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest where rel = note ("no relation between " <> parentTable <> " and " <> name) - $ findRelationTable schema name parentTable - <|> findRelationTable schema parentTable name - <|> findRelationColumn schema parentTable name + $ findRelationByTable schema name parentTable + <|> findRelationByTable schema parentTable name + <|> findRelationByColumn schema parentTable name addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation)) addRel (q, (n, _)) r = (q {from=fromRelation}, (n, Just r)) where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from q) @@ -136,9 +136,12 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _ _ -> Node (query, (name, Nothing)) <$> updatedForest where updatedForest = mapM (addRelations schema allRelations (Just node)) forest - findRelationTable s t1 t2 = - find (\r -> s == tableSchema (relTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations - findRelationColumn s t c = + -- Searches through all the relations and returns a match given the parameter conditions. + -- Will only find a relation where both schemas are in the PostgREST schema. + -- `findRelationByColumn` also does a ducktype check to see if the column name has any variation of `id` or `fk`. If so then the relation is returned as a match. + findRelationByTable s t1 t2 = + find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations + findRelationByColumn s t c = find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` (colName . head . relFColumns) r) allRelations where n `colMatches` rc = (cs ("^" <> rc <> "(?:|_id|Id)$") :: BS.ByteString) =~ (cs n :: BS.ByteString) From 7392d204bf7fe6db5f58f523b6f86f1602391a75 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 18:11:54 -0500 Subject: [PATCH 5/8] More inclusive foreign key ducktyping --- src/PostgREST/QueryBuilder.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index ec3407b58..a419ee03e 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -143,7 +143,7 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _ find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations findRelationByColumn s t c = find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` (colName . head . relFColumns) r) allRelations - where n `colMatches` rc = (cs ("^" <> rc <> "(?:|_id|Id)$") :: BS.ByteString) =~ (cs n :: BS.ByteString) + where n `colMatches` rc = (cs ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (cs n :: BS.ByteString) addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest addJoinConditions schema (Node (query, (n, r)) forest) = From 8f0ba7c41e4a5262ce25864ab1d77c04b057e371 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 13 Dec 2015 18:41:56 -0500 Subject: [PATCH 6/8] Unify JSON definition in tests --- test/Feature/DeleteSpec.hs | 3 ++- test/Feature/QuerySpec.hs | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/test/Feature/DeleteSpec.hs b/test/Feature/DeleteSpec.hs index c0f252728..23751141f 100644 --- a/test/Feature/DeleteSpec.hs +++ b/test/Feature/DeleteSpec.hs @@ -2,6 +2,7 @@ module Feature.DeleteSpec where import Test.Hspec import Test.Hspec.Wai +import Text.Heredoc import Hasql as H import Hasql.Postgres as P @@ -28,7 +29,7 @@ spec struct pool = beforeAll resetDb _ <- request methodDelete "/items?id=lt.15" [] "" get "/items" `shouldRespondWith` ResponseMatcher { - matchBody = Just "[{\"id\":15}]" + matchBody = Just [str|[{"id":15}]|] , matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-0/1"] } diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index dee897709..419652bdf 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -90,25 +90,25 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do get "/no_pk?a=is.null" `shouldRespondWith` [json| [{"a": null, "b": null}] |] - get "/nullable_integer?a=is.null" `shouldRespondWith` "[{\"a\":null}]" + get "/nullable_integer?a=is.null" `shouldRespondWith` [str|[{"a":null}]|] it "matches with like" $ do get "/simple_pk?k=like.*yx" `shouldRespondWith` - "[{\"k\":\"xyyx\",\"extra\":\"u\"}]" + [str|[{"k":"xyyx","extra":"u"}]|] get "/simple_pk?k=like.xy*" `shouldRespondWith` - "[{\"k\":\"xyyx\",\"extra\":\"u\"}]" + [str|[{"k":"xyyx","extra":"u"}]|] get "/simple_pk?k=like.*YY*" `shouldRespondWith` - "[{\"k\":\"xYYx\",\"extra\":\"v\"}]" + [str|[{"k":"xYYx","extra":"v"}]|] it "matches with like using not operator" $ get "/simple_pk?k=not.like.*yx" `shouldRespondWith` - "[{\"k\":\"xYYx\",\"extra\":\"v\"}]" + [str|[{"k":"xYYx","extra":"v"}]|] it "matches with ilike" $ do get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith` - "[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]" + [str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|] get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith` - "[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]" + [str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|] it "matches with ilike using not operator" $ get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]" @@ -131,7 +131,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do it "matches filtering nested items" $ get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith` - "[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1,\"name\":\"Design w7\"}]},{\"id\":2,\"tasks\":[{\"id\":3,\"name\":\"Design w10\"}]}]},{\"id\":2,\"projects\":[{\"id\":3,\"tasks\":[{\"id\":5,\"name\":\"Design IOS\"}]},{\"id\":4,\"tasks\":[{\"id\":7,\"name\":\"Design OSX\"}]}]}]" + [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|] it "matches with @> operator" $ get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith` @@ -192,15 +192,15 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do it "requesting parents and children" $ get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` - "[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]" + [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] it "requesting parents and filtering parent columns" $ get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith` - "[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1}}]" + [str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|] it "rows with missing parents are included" $ get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith` - "[{\"id\":1,\"clients\":{\"id\":1}},{\"id\":5,\"clients\":null}]" + [str|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|] it "rows with no children return [] instead of null" $ get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith` @@ -208,19 +208,19 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do it "requesting children 2 levels" $ get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith` - "[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2}]},{\"id\":2,\"tasks\":[{\"id\":3},{\"id\":4}]}]}]" + [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] it "requesting many<->many relation" $ get "/tasks?select=id,users{id}" `shouldRespondWith` - "[{\"id\":1,\"users\":[{\"id\":1},{\"id\":3}]},{\"id\":2,\"users\":[{\"id\":1}]},{\"id\":3,\"users\":[{\"id\":1}]},{\"id\":4,\"users\":[{\"id\":1}]},{\"id\":5,\"users\":[{\"id\":2},{\"id\":3}]},{\"id\":6,\"users\":[{\"id\":2}]},{\"id\":7,\"users\":[{\"id\":2}]},{\"id\":8,\"users\":[]}]" + [str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] it "requesting parents and children on views" $ get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` - "[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]" + [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] it "requesting children with composite key" $ 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\"}]}]" + [str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|] it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $ get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith` @@ -228,11 +228,11 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do it "can select by column name" $ get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith` - "[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client_id\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client_id\":{\"id\":2,\"name\":\"Apple\"}}]" + [str|[{"id":1,"name":"Windows 7","client_id":1,"client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client_id":{"id":2,"name":"Apple"}}]|] it "can select by column name sans id" $ get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith` - "[{\"id\":1,\"name\":\"Windows 7\",\"client_id\":1,\"client\":{\"id\":1,\"name\":\"Microsoft\"}},{\"id\":3,\"name\":\"IOS\",\"client_id\":2,\"client\":{\"id\":2,\"name\":\"Apple\"}}]" + [str|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|] describe "Plurality singular" $ do @@ -261,7 +261,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do 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\"}]}" + [str|{"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 From d1de6615f2f0a434732147ed48be639fed5105fd Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 16 Dec 2015 18:34:06 -0500 Subject: [PATCH 7/8] Add back in missing dependency after rebase --- src/PostgREST/QueryBuilder.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index a419ee03e..eab72ac17 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -48,6 +48,8 @@ import Data.Tree (Tree(..)) import qualified Data.Vector as V import PostgREST.Types import qualified Data.Map as M +import Text.Regex.TDFA ((=~)) +import qualified Data.ByteString.Char8 as BS import Data.Scientific ( FPFormat (..) , formatScientific , isInteger From f85975f5adf285312089fe22d86325506654b5ea Mon Sep 17 00:00:00 2001 From: calebmer Date: Wed, 16 Dec 2015 18:37:35 -0500 Subject: [PATCH 8/8] Add docs for column embedding --- docs/api/reading.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/api/reading.md b/docs/api/reading.md index 5e5fb5b5f..13bd913a9 100644 --- a/docs/api/reading.md +++ b/docs/api/reading.md @@ -227,10 +227,11 @@ Content-Range → 0-14/* ### Embedding Foreign Entities -Suppose you have a `projects` table which references `clients` through -a foreign key called `client_id`. When listing projects through the -API you can have it embed the client within each project response. -For example, +To help you make fewer requests, PostgREST allows the embedding of +traditional SQL relationships into a response. Suppose you have a +`projects` table which references `clients` through a foreign key +called `client_id`. When listing projects through the API you can +have it embed the client within each project response. For example, ```HTTP GET /projects?id=eq.1&select=id, name, clients{*} @@ -257,6 +258,33 @@ but the the select query is recursive. You could for instance specify GET /foo?select=x, y, bar{z, w, baz{*}} ``` +You can select not only using table names, but also column names! +To embed the same foreign key row from our client example earlier +you could do the following: + +```HTTP +GET /projects?id=eq.1&select=id, name, client_id{*} +``` + +In the response there will be a `client_id` object containing all +the data for that row. + +However, a `client_id` object doesn't make a lot of sense, so you +could do one of two things. Create a view which renames `client_id` +to just `client` (this is the hard way), or just try `client{*}` +in the select parameter! PostgREST supports smart ducktype checking +for common foreign key names, so if your column name ends with +`_id`, `_fk`, or any variation of the two (including camelcase) +you can embed a row with just the name's beginning. + +So for a complete example: + +```HTTP +GET /projects?id=eq.1&select=id, name, client{*} +``` + +Would embed in the `client` key the row referenced with `client_id`. + ### Response Format Query responses default to JSON but you can get them in CSV as well. Just make your request with the header