Support node/column renaming #310

This commit is contained in:
Ruslan Talpa
2016-05-12 10:22:19 +03:00
parent cacc725e41
commit c13f0a369b
7 changed files with 138 additions and 75 deletions
+1 -3
View File
@@ -6,13 +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 "-" in column names - @ruslantalpa
- Support column/node renaming `alias:column` - @ruslantalpa
### Fixed ### Fixed
- Set transaction mode to READ when possible to support connecting to read replicas - @ruslantalpa
- 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>
+28 -30
View File
@@ -7,12 +7,10 @@ module PostgREST.App (
) where ) where
import Control.Applicative import Control.Applicative
import Control.Arrow ((***))
import Control.Monad (join)
import Data.Bifunctor (first) import Data.Bifunctor (first)
import Data.List (find, sortBy, delete) import Data.IORef (IORef, readIORef)
import Data.List (find, delete)
import Data.Maybe (isJust, fromMaybe, fromJust, mapMaybe) import Data.Maybe (isJust, fromMaybe, fromJust, mapMaybe)
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange) import Data.Ranged.Ranges (emptyRange)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip) import Data.Text (Text, replace, strip)
@@ -24,10 +22,8 @@ import qualified Hasql.Transaction as HT
import Text.Parsec.Error import Text.Parsec.Error
import Text.ParserCombinators.Parsec (parse) import Text.ParserCombinators.Parsec (parse)
import Network.HTTP.Base (urlEncodeVars)
import Network.HTTP.Types.Header import Network.HTTP.Types.Header
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout) import Network.Wai.Middleware.RequestLogger (logStdout)
@@ -64,29 +60,31 @@ import PostgREST.Types
import Prelude import Prelude
transactionMode :: Action -> H.Mode postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> Application
transactionMode ActionRead = HT.Read postgrest conf refDbStructure pool =
transactionMode ActionInfo = HT.Read
transactionMode _ = HT.Write
postgrest :: AppConfig -> DbStructure -> P.Pool -> Application
postgrest conf dbStructure pool =
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
middle $ \ req respond -> do middle $ \ req respond -> do
time <- getPOSIXTime time <- getPOSIXTime
body <- strictRequestBody req body <- strictRequestBody req
dbStructure <- readIORef refDbStructure
let schema = cs $ configSchema conf let schema = cs $ configSchema conf
apiRequest = userApiRequest schema req body apiRequest = userApiRequest schema req body
handleReq = runWithClaims conf time (app dbStructure conf apiRequest) req handleReq = runWithClaims conf time (app dbStructure conf) apiRequest
txMode = transactionMode $ iAction apiRequest
resp <- either pgErrResponse id <$> P.use pool resp <- either pgErrResponse id <$> P.use pool
(HT.run handleReq HT.ReadCommitted (transactionMode $ iAction apiRequest)) (HT.run handleReq HT.ReadCommitted txMode)
respond resp respond resp
app :: DbStructure -> AppConfig -> ApiRequest -> Request -> H.Transaction Response transactionMode :: Action -> H.Mode
app dbStructure conf apiRequest req = transactionMode ActionRead = HT.Read
transactionMode ActionInfo = HT.Read
transactionMode _ = HT.Write
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response
app dbStructure conf apiRequest =
let let
-- TODO: blow up for Left values (there is a middleware that checks the headers) -- TODO: blow up for Left values (there is a middleware that checks the headers)
contentType = either (const ApplicationJSON) id (iAccepts apiRequest) contentType = either (const ApplicationJSON) id (iAccepts apiRequest)
@@ -110,11 +108,7 @@ app dbStructure conf apiRequest req =
else responseLBS status200 [contentTypeH] (cs body) else responseLBS status200 [contentTypeH] (cs body)
else do else do
let (status, contentRange) = rangeHeader queryTotal tableTotal let (status, contentRange) = rangeHeader queryTotal tableTotal
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)? canonical = iCanonicalQS apiRequest
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status return $ responseLBS status
[contentTypeH, contentRange, [contentTypeH, contentRange,
("Content-Location", ("Content-Location",
@@ -133,12 +127,14 @@ app dbStructure conf apiRequest req =
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
row <- H.query uniform stm row <- H.query uniform stm
let (_, _, location, body) = extractQueryResult row let (_, _, location, body) = extractQueryResult row
return $ responseLBS status201
[ return $ if iPreferRepresentation apiRequest == Full
contentTypeH, then responseLBS status201 [
(hLocation, "/" <> cs table <> "?" <> cs location) contentTypeH,
] (hLocation, "/" <> cs table <> "?" <> cs location)
$ if iPreferRepresentation apiRequest == Full then cs body else "" ] (cs body)
else responseLBS status201
[(hLocation, "/" <> cs table <> "?" <> cs location)] ""
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) -> (ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) ->
case mutateSqlParts of case mutateSqlParts of
@@ -151,8 +147,9 @@ app dbStructure conf apiRequest req =
s = case () of _ | queryTotal == 0 -> status404 s = case () of _ | queryTotal == 0 -> status404
| iPreferRepresentation apiRequest == Full -> status200 | iPreferRepresentation apiRequest == Full -> status200
| otherwise -> status204 | otherwise -> status204
return $ responseLBS s [contentTypeH, r] return $ if iPreferRepresentation apiRequest == Full
$ if iPreferRepresentation apiRequest == Full then cs body else "" then responseLBS s [contentTypeH, r] (cs body)
else responseLBS s [r] ""
(ActionDelete, TargetIdent qi, Nothing) -> (ActionDelete, TargetIdent qi, Nothing) ->
case mutateSqlParts of case mutateSqlParts of
@@ -346,6 +343,7 @@ addFilter (path, flt) (Node rn forest) =
maybeNode = find fnd forst maybeNode = find fnd forst
where where
fnd :: ReadRequest -> Bool 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
+27 -11
View File
@@ -12,16 +12,21 @@ 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,24 +56,20 @@ 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 = do pFieldName = do
matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) <?> "field name (* or [a..z0..9_])" matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) <?> "field name (* or [a..z0..9_])"
return $ intercalate "-" $ map cs matches return $ intercalate "-" $ map cs matches
where where
isDash :: GenParser Char st () isDash :: GenParser Char st ()
isDash = try (do{ isDash = try ( char '-' >> notFollowedBy (char '>') )
_ <- char '-'
; notFollowedBy (char '>')
})
dash :: Parser Char dash :: Parser Char
dash = isDash *> pure '-' dash = isDash *> pure '-'
@@ -82,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
+31 -1
View File
@@ -147,16 +147,25 @@ spec = do
it "dash `-` in column names is accepted" $ it "dash `-` in column names is accepted" $
get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith` get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`
[str|[{"id":3,"field-with_sep":1}]|] [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"}}}] |]
@@ -165,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 {
@@ -186,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}}]|]
@@ -215,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`