support dash in column names fix #462
This commit is contained in:
@@ -8,8 +8,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Reload database schema on SIGHUP - @begriffs
|
- Reload database schema on SIGHUP - @begriffs
|
||||||
|
- Support "-" in column names - @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
|
||||||
|
|||||||
+43
-35
@@ -7,10 +7,12 @@ 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.IORef (IORef, readIORef)
|
import Data.List (find, sortBy, delete)
|
||||||
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)
|
||||||
@@ -22,8 +24,10 @@ 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)
|
||||||
|
|
||||||
@@ -60,31 +64,29 @@ import PostgREST.Types
|
|||||||
import Prelude
|
import Prelude
|
||||||
|
|
||||||
|
|
||||||
postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> Application
|
|
||||||
postgrest conf refDbStructure pool =
|
|
||||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
|
||||||
|
|
||||||
middle $ \ req respond -> do
|
|
||||||
time <- getPOSIXTime
|
|
||||||
body <- strictRequestBody req
|
|
||||||
dbStructure <- readIORef refDbStructure
|
|
||||||
|
|
||||||
let schema = cs $ configSchema conf
|
|
||||||
apiRequest = userApiRequest schema req body
|
|
||||||
handleReq = runWithClaims conf time (app dbStructure conf) apiRequest
|
|
||||||
txMode = transactionMode $ iAction apiRequest
|
|
||||||
|
|
||||||
resp <- either pgErrResponse id <$> P.use pool
|
|
||||||
(HT.run handleReq HT.ReadCommitted txMode)
|
|
||||||
respond resp
|
|
||||||
|
|
||||||
transactionMode :: Action -> H.Mode
|
transactionMode :: Action -> H.Mode
|
||||||
transactionMode ActionRead = HT.Read
|
transactionMode ActionRead = HT.Read
|
||||||
transactionMode ActionInfo = HT.Read
|
transactionMode ActionInfo = HT.Read
|
||||||
transactionMode _ = HT.Write
|
transactionMode _ = HT.Write
|
||||||
|
|
||||||
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response
|
postgrest :: AppConfig -> DbStructure -> P.Pool -> Application
|
||||||
app dbStructure conf apiRequest =
|
postgrest conf dbStructure pool =
|
||||||
|
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
||||||
|
|
||||||
|
middle $ \ req respond -> do
|
||||||
|
time <- getPOSIXTime
|
||||||
|
body <- strictRequestBody req
|
||||||
|
|
||||||
|
let schema = cs $ configSchema conf
|
||||||
|
apiRequest = userApiRequest schema req body
|
||||||
|
handleReq = runWithClaims conf time (app dbStructure conf apiRequest) req
|
||||||
|
|
||||||
|
resp <- either pgErrResponse id <$> P.use pool
|
||||||
|
(HT.run handleReq HT.ReadCommitted (transactionMode $ iAction apiRequest))
|
||||||
|
respond resp
|
||||||
|
|
||||||
|
app :: DbStructure -> AppConfig -> ApiRequest -> Request -> H.Transaction Response
|
||||||
|
app dbStructure conf apiRequest req =
|
||||||
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)
|
||||||
@@ -108,7 +110,11 @@ app dbStructure conf apiRequest =
|
|||||||
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 = iCanonicalQS apiRequest
|
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
||||||
|
. sortBy (comparing fst)
|
||||||
|
. map (join (***) cs)
|
||||||
|
. parseSimpleQuery
|
||||||
|
$ rawQueryString req
|
||||||
return $ responseLBS status
|
return $ responseLBS status
|
||||||
[contentTypeH, contentRange,
|
[contentTypeH, contentRange,
|
||||||
("Content-Location",
|
("Content-Location",
|
||||||
@@ -127,14 +133,12 @@ app dbStructure conf apiRequest =
|
|||||||
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
|
[
|
||||||
then responseLBS status201 [
|
contentTypeH,
|
||||||
contentTypeH,
|
(hLocation, "/" <> cs table <> "?" <> cs location)
|
||||||
(hLocation, "/" <> cs table <> "?" <> cs location)
|
]
|
||||||
] (cs body)
|
$ if iPreferRepresentation apiRequest == Full then cs body else ""
|
||||||
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
|
||||||
@@ -147,9 +151,8 @@ app dbStructure conf apiRequest =
|
|||||||
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 $ if iPreferRepresentation apiRequest == Full
|
return $ responseLBS s [contentTypeH, r]
|
||||||
then responseLBS s [contentTypeH, r] (cs body)
|
$ if iPreferRepresentation apiRequest == Full then cs body else ""
|
||||||
else responseLBS s [r] ""
|
|
||||||
|
|
||||||
(ActionDelete, TargetIdent qi, Nothing) ->
|
(ActionDelete, TargetIdent qi, Nothing) ->
|
||||||
case mutateSqlParts of
|
case mutateSqlParts of
|
||||||
@@ -333,11 +336,16 @@ 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
|
||||||
|
|
||||||
-- 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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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
|
||||||
@@ -57,9 +57,21 @@ pFieldTree = try (Node <$> pSelect <*> between (char '{') (char '}') pFieldFores
|
|||||||
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 (do{
|
||||||
|
_ <- 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)
|
||||||
|
|||||||
@@ -143,7 +143,11 @@ 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`
|
||||||
|
|||||||
Vendored
+2
-1
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user