Support node/column renaming #310
This commit is contained in:
+1
-3
@@ -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
@@ -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
@@ -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
@@ -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, ...)")
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user