Merge pull request #424 from calebmer/feature/select-column

Enable selection by column
This commit is contained in:
Joe Nelson
2015-12-16 16:09:25 -08:00
6 changed files with 95 additions and 45 deletions
+1
View File
@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added ### Added
- Allow order by computed columns - @diogob - Allow order by computed columns - @diogob
- Set max rows in response with --max-rows - @begriffs - 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 ## [0.3.0.1] - 2015-11-27
+32 -4
View File
@@ -227,10 +227,11 @@ Content-Range → 0-14/*
### Embedding Foreign Entities ### Embedding Foreign Entities
Suppose you have a `projects` table which references `clients` through To help you make fewer requests, PostgREST allows the embedding of
a foreign key called `client_id`. When listing projects through the traditional SQL relationships into a response. Suppose you have a
API you can have it embed the client within each project response. `projects` table which references `clients` through a foreign key
For example, called `client_id`. When listing projects through the API you can
have it embed the client within each project response. For example,
```HTTP ```HTTP
GET /projects?id=eq.1&select=id, name, clients{*} 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{*}} 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 ### Response Format
Query responses default to JSON but you can get them in CSV as well. Just make your request with the header Query responses default to JSON but you can get them in CSV as well. Just make your request with the header
+31 -19
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-| {-|
Module : PostgREST.QueryBuilder Module : PostgREST.QueryBuilder
@@ -47,6 +48,8 @@ import Data.Tree (Tree(..))
import qualified Data.Vector as V import qualified Data.Vector as V
import PostgREST.Types import PostgREST.Types
import qualified Data.Map as M import qualified Data.Map as M
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import Data.Scientific ( FPFormat (..) import Data.Scientific ( FPFormat (..)
, formatScientific , formatScientific
, isInteger , isInteger
@@ -120,20 +123,29 @@ createWriteStatement qi selectQuery mutateQuery isSingle Full
| otherwise = asJsonF | otherwise = asJsonF
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest 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 case parentNode of
Nothing -> Node (query, (table, Nothing)) <$> updatedForest (Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest
(Just (Node (_, (parentTable, _)) _)) -> Node <$> (addRel n <$> rel) <*> updatedForest
where where
rel = note ("no relation between " <> table <> " and " <> parentTable) rel = note ("no relation between " <> parentTable <> " and " <> name)
$ findRelation schema table parentTable $ findRelationByTable schema name parentTable
<|> findRelation schema parentTable table <|> findRelationByTable schema parentTable name
<|> findRelationByColumn schema parentTable name
addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation)) 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 where
updatedForest = mapM (addRelations schema allRelations (Just node)) forest updatedForest = mapM (addRelations schema allRelations (Just node)) forest
findRelation s t1 t2 = -- Searches through all the relations and returns a match given the parameter conditions.
find (\r -> s == (tableSchema . relTable) r && t1 == (tableName . relTable) r && t2 == (tableName . relFTable) r) allRelations -- 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 <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (cs n :: BS.ByteString)
addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest
addJoinConditions schema (Node (query, (n, r)) forest) = addJoinConditions schema (Node (query, (n, r)) forest) =
@@ -218,11 +230,12 @@ requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _))
requestToQuery :: Schema -> DbRequest -> SqlQuery requestToQuery :: Schema -> DbRequest -> SqlQuery
requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined
requestToQuery _ (DbMutate (Update _ (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 query
where where
-- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name -- 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 -- of our WITH query part
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
tblSchema tbl = if tbl == sourceCTEName then "" else schema tblSchema tbl = if tbl == sourceCTEName then "" else schema
qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl
toQi t = QualifiedIdentifier (tblSchema t) t toQi t = QualifiedIdentifier (tblSchema t) t
@@ -253,28 +266,27 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (mai
intercalate " AND " ( map (pgFmtCondition qi ) joinConditions ) intercalate " AND " ( map (pgFmtCondition qi ) joinConditions )
where where
joinConditions = filter (filterParentConditions t) conditions joinConditions = filter (filterParentConditions t) conditions
filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t
parentTable == t
filterParentConditions _ _ = False filterParentConditions _ _ = False
getQueryParts :: Tree ReadNode -> ([(SqlFragment, TableName)], [SqlFragment]) -> ([(SqlFragment,TableName)], [SqlFragment]) 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 where
sel = "COALESCE((" sel = "COALESCE(("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) " <> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table <> "FROM (" <> subquery <> ") " <> table
<> "), '[]') AS " <> table <> "), '[]') AS " <> pgFmtIdent name
where subquery = requestToQuery schema (DbRead (Node n forst)) 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 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) joi = ("( " <> subquery <> " ) AS " <> table, table)
where subquery = requestToQuery schema (DbRead (Node n forst)) 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 where
sel = "COALESCE ((" sel = "COALESCE (("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) " <> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table <> "FROM (" <> subquery <> ") " <> table
<> "), '[]') AS " <> table <> "), '[]') AS " <> pgFmtIdent name
where subquery = requestToQuery schema (DbRead (Node n forst)) where subquery = requestToQuery schema (DbRead (Node n forst))
--the following is just to remove the warning --the following is just to remove the warning
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only --getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
+4 -4
View File
@@ -106,10 +106,10 @@ type Cast = Text
type NodeName = Text type NodeName = Text
type SelectItem = (Field, Maybe Cast) type SelectItem = (Field, Maybe Cast)
type Path = [Text] type Path = [Text]
data ReadQuery = Select { select::[SelectItem], from::[Text], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq) data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq)
data MutateQuery = Insert { in_::Text, qPayload::Payload } data MutateQuery = Insert { in_::TableName, qPayload::Payload }
| Delete { in_::Text, where_::[Filter] } | Delete { in_::TableName, where_::[Filter] }
| Update { in_::Text, qPayload::Payload, where_::[Filter] } deriving (Show, Eq) | Update { in_::TableName, qPayload::Payload, where_::[Filter] } deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq) data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
type ReadNode = (ReadQuery, (NodeName, Maybe Relation)) type ReadNode = (ReadQuery, (NodeName, Maybe Relation))
type ReadRequest = Tree ReadNode type ReadRequest = Tree ReadNode
+2 -1
View File
@@ -2,6 +2,7 @@ module Feature.DeleteSpec where
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Text.Heredoc
import Hasql as H import Hasql as H
import Hasql.Postgres as P import Hasql.Postgres as P
@@ -28,7 +29,7 @@ spec struct pool = beforeAll resetDb
_ <- request methodDelete "/items?id=lt.15" [] "" _ <- request methodDelete "/items?id=lt.15" [] ""
get "/items" get "/items"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]" matchBody = Just [str|[{"id":15}]|]
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"] , matchHeaders = ["Content-Range" <:> "0-0/1"]
} }
+25 -17
View File
@@ -90,25 +90,25 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
get "/no_pk?a=is.null" `shouldRespondWith` get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |] [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 it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith` get "/simple_pk?k=like.*yx" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]" [str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.xy*" `shouldRespondWith` get "/simple_pk?k=like.xy*" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]" [str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.*YY*" `shouldRespondWith` get "/simple_pk?k=like.*YY*" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]" [str|[{"k":"xYYx","extra":"v"}]|]
it "matches with like using not operator" $ it "matches with like using not operator" $
get "/simple_pk?k=not.like.*yx" `shouldRespondWith` get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]" [str|[{"k":"xYYx","extra":"v"}]|]
it "matches with ilike" $ do it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith` 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` 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" $ it "matches with ilike using not operator" $
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]" 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" $ it "matches filtering nested items" $
get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith` 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" $ it "matches with @> operator" $
get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith` 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" $ it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` 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" $ it "requesting parents and filtering parent columns" $
get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith` 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" $ it "rows with missing parents are included" $
get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith` 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" $ it "rows with no children return [] instead of null" $
get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith` get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith`
@@ -208,23 +208,31 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
it "requesting children 2 levels" $ it "requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith` 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" $ it "requesting many<->many relation" $
get "/tasks?select=id,users{id}" `shouldRespondWith` 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" $ it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` 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" $ it "requesting children with composite key" $
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith` 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" $ 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` 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`
[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`
[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 describe "Plurality singular" $ do
@@ -253,7 +261,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
it "can shape plurality singular object routes" $ it "can shape plurality singular object routes" $
request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [("Prefer","plurality=singular")] "" request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [("Prefer","plurality=singular")] ""
`shouldRespondWith` `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 describe "ordering response" $ do