Merge pull request #586 from ruslantalpa/rename_order_limit_feature

Ability to rename columns/nodes in the output and support "-" in column names
This commit is contained in:
Joe Nelson
2016-05-15 00:55:12 -07:00
8 changed files with 134 additions and 44 deletions
+2 -2
View File
@@ -6,11 +6,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
### Added ### Added
- Reload database schema on SIGHUP - @begriffs - Reload database schema on SIGHUP - @begriffs
- Support "-" in column names - @ruslantalpa
- Support column/node renaming `alias:column` - @ruslantalpa
### Fixed ### Fixed
- Omit Content-Type header for empty body - @begriffs - Omit Content-Type header for empty body - @begriffs
- Prevent role from being changed twice - @begriffs - Prevent role from being changed twice - @begriffs
- Use read-only transaction for read requests - @ruslantalpa - Use read-only transaction for read requests - @ruslantalpa
+22 -3
View File
@@ -258,7 +258,8 @@ 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! You can select not only using table names, but also foreign key column names!
This is especially needed when you have a table with two foreign keys pointing to the same table, for example billing_address_id and shipping_address_id.
To embed the same foreign key row from our client example earlier To embed the same foreign key row from our client example earlier
you could do the following: you could do the following:
@@ -270,8 +271,7 @@ In the response there will be a `client_id` object containing all
the data for that row. the data for that row.
However, a `client_id` object doesn't make a lot of sense, so you 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` could do one of two things. Tell PostgREST that you want the key renamed by using the `alias` feature like so `client:client_id{*}`, or just try `client{*}`
to just `client` (this is the hard way), or just try `client{*}`
in the select parameter! PostgREST supports smart ducktype checking in the select parameter! PostgREST supports smart ducktype checking
for common foreign key names, so if your column name ends with for common foreign key names, so if your column name ends with
`_id`, `_fk`, or any variation of the two (including camelcase) `_id`, `_fk`, or any variation of the two (including camelcase)
@@ -285,6 +285,25 @@ GET /projects?id=eq.1&select=id, name, client{*}
Would embed in the `client` key the row referenced with `client_id`. 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 following request will produce the output below:
```HTTP
GET /orders?id=eq.1&select=orderId:id, customer:customer_id{customerId:id, customerName:name}
```
```json
[
{
"orderId": 1,
"customer": {
"customerId": 1,
"customerName": "John Smith"
}
}
]
```
<div class="admonition note"> <div class="admonition note">
<p class="admonition-title">Design Consideration</p> <p class="admonition-title">Design Consideration</p>
<p>In order for this feature to work as expected after a schema change, PostgREST currently requires to be restarted.</p> <p>In order for this feature to work as expected after a schema change, PostgREST currently requires to be restarted.</p>
+7 -1
View File
@@ -333,11 +333,17 @@ addFilter (path, flt) (Node rn forest) =
where where
targetNodeName:remainingPath = path targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest (targetNode,restForest) = splitForest targetNodeName forest
splitForest :: NodeName -> Forest ReadNode -> (Maybe ReadRequest, Forest ReadNode)
splitForest name forst = splitForest name forst =
case maybeNode of case maybeNode of
Nothing -> (Nothing,forest) Nothing -> (Nothing,forest)
Just node -> (Just node, delete node forest) Just node -> (Just node, delete node forest)
where maybeNode = find ((name==).fst.snd.rootLabel) forst where
maybeNode :: Maybe ReadRequest
maybeNode = find fnd forst
where
fnd :: ReadRequest -> Bool
fnd (Node (_,(n,_,_)) _) = n == name
-- in a relation where one of the tables mathces "TableName" -- in a relation where one of the tables mathces "TableName"
-- replace the name to that table with pg_source -- replace the name to that table with pg_source
+37 -9
View File
@@ -6,22 +6,27 @@ where
import Control.Applicative hiding ((<$>)) import Control.Applicative hiding ((<$>))
import Data.Monoid import Data.Monoid
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text) import Data.Text (Text, intercalate)
import Data.Tree import Data.Tree
import PostgREST.QueryBuilder (operators) import PostgREST.QueryBuilder (operators)
import PostgREST.Types import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>)) import Text.ParserCombinators.Parsec hiding (many, (<|>))
pRequestSelect :: Text -> Parser ReadRequest pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do pRequestSelect rootNodeName = do
fieldTree <- pFieldForest fieldTree <- pFieldForest
return $ foldr treeEntry (Node (Select [] [rootNodeName] [] Nothing, (rootNodeName, Nothing)) []) fieldTree return $ foldr treeEntry (Node (readQuery, (rootNodeName, Nothing, Nothing)) []) fieldTree
where where
readQuery = Select [] [rootNodeName] [] Nothing
treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry (Node fld@((fn, _),_) fldForest) (Node (q, i) rForest) = treeEntry (Node fld@((fn, _),_,alias) fldForest) (Node (q, i) rForest) =
case fldForest of case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest [] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) (foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, Nothing)) []) fldForest:rForest) _ -> Node (q, i) newForest
where
newForest =
foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, Nothing, alias)) []) fldForest:rForest
pRequestFilter :: (String, String) -> Either ParseError (Path, Filter) pRequestFilter :: (String, String) -> Either ParseError (Path, Filter)
pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val) pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
@@ -51,15 +56,23 @@ pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',') pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
pFieldTree :: Parser (Tree SelectItem) pFieldTree :: Parser (Tree SelectItem)
pFieldTree = try (Node <$> pSelect <*> between (char '{') (char '}') pFieldForest) pFieldTree = try (Node <$> pSimpleSelect <*> between (char '{') (char '}') pFieldForest)
<|> Node <$> pSelect <*> pure [] <|> Node <$> pSelect <*> pure []
pStar :: Parser Text pStar :: Parser Text
pStar = cs <$> (string "*" *> pure ("*"::String)) pStar = cs <$> (string "*" *> pure ("*"::String))
pFieldName :: Parser Text pFieldName :: Parser Text
pFieldName = cs <$> (many1 (letter <|> digit <|> oneOf "_") pFieldName = do
<?> "field name (* or [a..z0..9_])") matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) <?> "field name (* or [a..z0..9_])"
return $ intercalate "-" $ map cs matches
where
isDash :: GenParser Char st ()
isDash = try ( char '-' >> notFollowedBy (char '>') )
dash :: Parser Char
dash = isDash *> pure '-'
pJsonPathStep :: Parser Text pJsonPathStep :: Parser Text
pJsonPathStep = cs <$> try (string "->" *> pFieldName) pJsonPathStep = cs <$> try (string "->" *> pFieldName)
@@ -70,12 +83,27 @@ pJsonPath = (++) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldN
pField :: Parser Field pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath
aliasSeparator = char ':' >> notFollowedBy (char ':')
pSimpleSelect :: Parser SelectItem
pSimpleSelect = lexeme $ try ( do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
return (fld, Nothing, alias)
)
pSelect :: Parser SelectItem pSelect :: Parser SelectItem
pSelect = lexeme $ pSelect = lexeme $
try ((,) <$> pField <*>((cs <$>) <$> optionMaybe (string "::" *> many letter)) ) try (
do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast <- optionMaybe (string "::" *> many letter)
return (fld, cs <$> cast, alias)
)
<|> do <|> do
s <- pStar s <- pStar
return ((s, Nothing), Nothing) return ((s, Nothing), Nothing, Nothing)
pOperator :: Parser Operator pOperator :: Parser Operator
pOperator = cs <$> (pOp <?> "operator (eq, gt, ...)") pOperator = cs <$> (pOp <?> "operator (eq, gt, ...)")
+26 -25
View File
@@ -158,18 +158,18 @@ 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 readNode@(query, (name, _)) forest) = addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _, alias)) forest) =
case parentNode of case parentNode of
(Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest (Just (Node (Select{from=[parentTable]}, (_, _, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest
where where
rel = note ("no relation between " <> parentTable <> " and " <> name) rel = note ("no relation between " <> parentTable <> " and " <> name)
$ findRelationByTable schema name parentTable $ findRelationByTable schema name parentTable
<|> findRelationByColumn schema parentTable name <|> findRelationByColumn schema parentTable name
addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation)) addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
addRel (query', (n, _)) r = (query' {from=fromRelation}, (n, Just r)) addRel (query', (n, _, a)) r = (query' {from=fromRelation}, (n, Just r, a))
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query') where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query')
_ -> Node (query, (name, Nothing)) <$> updatedForest _ -> Node (query, (name, Nothing, alias)) <$> updatedForest
where where
updatedForest = mapM (addRelations schema allRelations (Just node)) forest updatedForest = mapM (addRelations schema allRelations (Just node)) forest
-- Searches through all the relations and returns a match given the parameter conditions. -- Searches through all the relations and returns a match given the parameter conditions.
@@ -182,13 +182,13 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _
where n `colMatches` rc = (cs ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: 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 -> ReadRequest -> Either Text ReadRequest
addJoinConditions schema (Node (query, (n, r)) forest) = addJoinConditions schema (Node (query, (n, r, a)) forest) =
case r of case r of
Nothing -> Node (updatedQuery, (n,r)) <$> updatedForest -- this is the root node Nothing -> Node (updatedQuery, (n,r,a)) <$> updatedForest -- this is the root node
Just rel@Relation{relType=Child} -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest Just rel@Relation{relType=Child} -> Node (addCond updatedQuery (getJoinConditions rel),(n,r,a)) <$> updatedForest
Just Relation{relType=Parent} -> Node (updatedQuery, (n,r)) <$> updatedForest Just Relation{relType=Parent} -> Node (updatedQuery, (n,r,a)) <$> updatedForest
Just rel@Relation{relType=Many, relLTable=(Just linkTable)} -> Just rel@Relation{relType=Many, relLTable=(Just linkTable)} ->
Node (qq, (n, r)) <$> updatedForest Node (qq, (n, r, a)) <$> updatedForest
where where
query' = addCond updatedQuery (getJoinConditions rel) query' = addCond updatedQuery (getJoinConditions rel)
qq = query'{from=tableName linkTable : from query'} qq = query'{from=tableName linkTable : from query'}
@@ -199,7 +199,7 @@ addJoinConditions schema (Node (query, (n, r)) forest) =
where where
parentJoinConditions = map (getJoinConditions . snd) parents parentJoinConditions = map (getJoinConditions . snd) parents
parents = mapMaybe (getParents . rootLabel) forest parents = mapMaybe (getParents . rootLabel) forest
getParents (_, (tbl, Just rel@Relation{relType=Parent})) = Just (tbl, rel) getParents (_, (tbl, Just rel@Relation{relType=Parent}, _)) = Just (tbl, rel)
getParents _ = Nothing getParents _ = Nothing
updatedForest = mapM (addJoinConditions schema) forest updatedForest = mapM (addJoinConditions schema) forest
addCond query' con = query'{flt_=con ++ flt_ query'} addCond query' con = query'{flt_=con ++ flt_ query'}
@@ -262,7 +262,7 @@ pgFmtLit x =
requestToCountQuery :: Schema -> DbRequest -> SqlQuery requestToCountQuery :: Schema -> DbRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = undefined requestToCountQuery _ (DbMutate _) = undefined
requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) _)) = requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _, _)) _)) =
unwords [ unwords [
"SELECT pg_catalog.count(1)", "SELECT pg_catalog.count(1)",
"FROM ", fromQi $ QualifiedIdentifier schema mainTbl, "FROM ", fromQi $ QualifiedIdentifier schema mainTbl,
@@ -276,7 +276,7 @@ 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, (nodeName, maybeRelation)) 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
@@ -315,29 +315,29 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nod
filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" 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@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s)
where where
sel = "COALESCE((" sel = "COALESCE(("
<> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) " <> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent name <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = requestToQuery schema (DbRead (Node n forst)) where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}})) forst) (j,s) = (joi:j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (joi:j,sel:s)
where where
sel = "row_to_json(" <> pgFmtIdent table <> ".*) AS "<>pgFmtIdent name --TODO must be singular sel = "row_to_json(" <> pgFmtIdent table <> ".*) AS " <> pgFmtIdent (fromMaybe name alias)
joi = ("( " <> subquery <> " ) AS " <> pgFmtIdent table, table) joi = ("( " <> subquery <> " ) AS " <> pgFmtIdent table, table)
where subquery = requestToQuery schema (DbRead (Node n forst)) where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s)
where where
sel = "COALESCE ((" sel = "COALESCE (("
<> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) " <> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent name <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
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
--posible relations are Child Parent Many --posible relations are Child Parent Many
getQueryParts (Node (_,(_,Nothing)) _) _ = undefined getQueryParts (Node (_,(_,Nothing,_)) _) _ = undefined
requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) = requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) =
let qi = QualifiedIdentifier schema mainTbl let qi = QualifiedIdentifier schema mainTbl
cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0)) cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0))
@@ -463,8 +463,8 @@ pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
pgFmtSelectItem table (f@(_, jp), Nothing) = pgFmtField table f <> pgFmtAsJsonPath jp pgFmtSelectItem table (f@(_, jp), Nothing, alias) = pgFmtField table f <> pgFmtAs jp alias
pgFmtSelectItem table (f@(_, jp), Just cast ) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAsJsonPath jp pgFmtSelectItem table (f@(_, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias
pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment
pgFmtCondition table (Filter (col,jp) ops val) = pgFmtCondition table (Filter (col,jp) ops val) =
@@ -511,9 +511,10 @@ pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs ) pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = "" pgFmtJsonPath _ = ""
pgFmtAsJsonPath :: Maybe JsonPath -> SqlFragment pgFmtAs :: Maybe JsonPath -> Maybe Alias -> SqlFragment
pgFmtAsJsonPath Nothing = "" pgFmtAs Nothing Nothing = ""
pgFmtAsJsonPath (Just xx) = " AS " <> last xx pgFmtAs (Just xx) Nothing = " AS " <> pgFmtIdent (last xx)
pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias
trimNullChars :: Text -> Text trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
+3 -2
View File
@@ -106,16 +106,17 @@ data FValue = VText Text | VForeignKey QualifiedIdentifier ForeignKey deriving (
type FieldName = Text type FieldName = Text
type JsonPath = [Text] type JsonPath = [Text]
type Field = (FieldName, Maybe JsonPath) type Field = (FieldName, Maybe JsonPath)
type Alias = Text
type Cast = Text type Cast = Text
type NodeName = Text type NodeName = Text
type SelectItem = (Field, Maybe Cast) type SelectItem = (Field, Maybe Cast, Maybe Alias)
type Path = [Text] type Path = [Text]
data ReadQuery = Select { select::[SelectItem], from::[TableName], 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_::TableName, qPayload::Payload } data MutateQuery = Insert { in_::TableName, qPayload::Payload }
| Delete { in_::TableName, where_::[Filter] } | Delete { in_::TableName, where_::[Filter] }
| Update { in_::TableName, 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, Maybe Alias))
type ReadRequest = Tree ReadNode type ReadRequest = Tree ReadNode
type MutateRequest = MutateQuery type MutateRequest = MutateQuery
data DbRequest = DbRead ReadRequest | DbMutate MutateRequest data DbRequest = DbRead ReadRequest | DbMutate MutateRequest
+35 -1
View File
@@ -143,16 +143,29 @@ spec = do
it "selectStar works in absense of parameter" $ it "selectStar works in absense of parameter" $
get "/complex_items?id=eq.3" `shouldRespondWith` get "/complex_items?id=eq.3" `shouldRespondWith`
[str|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3]}]|] [str|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}]|]
it "dash `-` in column names is accepted" $
get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`
[str|[{"id":3,"field-with_sep":1}]|]
it "one simple column" $ it "one simple column" $
get "/complex_items?select=id" `shouldRespondWith` get "/complex_items?select=id" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3}] |] [json| [{"id":1},{"id":2},{"id":3}] |]
it "rename simple column" $
get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith`
[json| [{"myId":1}] |]
it "one simple column with casting (text)" $ it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith` get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |] [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
it "rename simple column with casting" $
get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith`
[json| [{"myId":"1"}] |]
it "json column" $ it "json column" $
get "/complex_items?id=eq.1&select=settings" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |] [json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
@@ -161,6 +174,10 @@ spec = do
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text" [json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
it "rename json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
it "fails on bad casting (data of the wrong format)" $ it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer" get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
@@ -182,15 +199,28 @@ spec = do
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |] [json| [{"bar":"baz"}] |]
it "rename json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
[json| [{"myBar":"baz"}] |]
it "json subfield two levels with casting (int)" $ it "json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now [json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
it "rename json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
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`
[str|[{"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 children while renaming them" $
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith`
[str|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_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`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|] [str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]
@@ -211,6 +241,10 @@ spec = do
get "/tasks?select=id,users{id}" `shouldRespondWith` get "/tasks?select=id,users{id}" `shouldRespondWith`
[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":[]}]|] [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 many<->many relation with rename" $
get "/tasks?id=eq.1&select=id,theUsers:users{id}" `shouldRespondWith`
[str|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|]
it "requesting many<->many relation reverse" $ it "requesting many<->many relation reverse" $
get "/users?select=id,tasks{id}" `shouldRespondWith` get "/users?select=id,tasks{id}" `shouldRespondWith`
+2 -1
View File
@@ -432,7 +432,8 @@ CREATE TABLE complex_items (
id bigint NOT NULL, id bigint NOT NULL,
name text, name text,
settings pg_catalog.json, settings pg_catalog.json,
arr_data integer[] arr_data integer[],
"field-with_sep" integer default 1 not null
); );