Merge remote-tracking branch 'begriffs/master'

This commit is contained in:
Ruslan Talpa
2015-09-07 09:28:31 +03:00
14 changed files with 232 additions and 189 deletions
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
### Added
- Filter columns, e.g. `?select=col1,col2` - @ruslantalpa
## [0.2.11.1] - 2015-09-01
### Fixed
- Accepts `*/*` in Accept header - @diogob
## [0.2.11.0] - 2015-08-28 ## [0.2.11.0] - 2015-08-28
### Added ### Added
- Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob - Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob
+11 -1
View File
@@ -10,7 +10,7 @@
}, },
"POSTGREST_VER": { "POSTGREST_VER": {
"description": "Version of PostgREST to deploy", "description": "Version of PostgREST to deploy",
"value": "0.2.11.0" "value": "0.2.11.1"
}, },
"DB_NAME": { "DB_NAME": {
"description": "Database name", "description": "Database name",
@@ -41,6 +41,16 @@
"description": "Maximum number of connections in database pool", "description": "Maximum number of connections in database pool",
"required": false, "required": false,
"value": "10" "value": "10"
},
"JWT_SECRET": {
"description": "Secret used to encrypt JSON Web Tokens",
"required": false,
"value": "secret"
},
"V1SCHEMA": {
"description": "DB schema selected whe no version (or version 1) requested",
"required": false,
"value": "1"
} }
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@ name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security for the tables and views, supporting all HTTP verbs that security
permits. permits.
version: 0.2.11.0 version: 0.2.11.1
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
+1 -1
View File
@@ -1,6 +1,6 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'` export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
curl -L http://softlayer-ams.dl.sourceforge.net/project/s3tools/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx curl -L http://sourceforge.net/projects/s3tools/files/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER} cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER} tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
+46 -44
View File
@@ -65,15 +65,15 @@ app conf reqBody req =
let qt = qualify table let qt = qualify table
from = fromMaybe 0 $ rangeOffset <$> range from = fromMaybe 0 $ rangeOffset <$> range
query = B.Stmt "select " V.empty True <> query = B.Stmt "select " V.empty True <>
parentheticT ( parentheticT (
whereT qt qq $ countRows qt whereT qt qq $ countRows qt
) <> commaq <> ( ) <> commaq <> (
bodyForAccept contentType qt bodyForAccept contentType qt
. limitT range . limitT range
. orderT (orderParse qq) . orderT (orderParse qq)
. whereT qt qq . whereT qt qq
$ select qt qq $ select qt qq
) )
row <- H.maybeEx query row <- H.maybeEx query
let (tableTotal, queryTotal, body) = let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row fromMaybe (0, 0, Just "" :: Maybe Text) row
@@ -81,14 +81,14 @@ app conf reqBody req =
contentRange = contentRangeH from to tableTotal contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal status = rangeStatus from to tableTotal
canonical = urlEncodeVars canonical = urlEncodeVars
. sortBy (comparing fst) . sortBy (comparing fst)
. map (join (***) cs) . map (join (***) cs)
. parseSimpleQuery . parseSimpleQuery
$ rawQueryString req $ rawQueryString req
return $ responseLBS status return $ responseLBS status
[contentTypeH, contentRange, [contentTypeH, contentRange,
("Content-Location", ("Content-Location",
"/" <> cs table <> "/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical if Prelude.null canonical then "" else "?" <> cs canonical
) )
] (cs $ fromMaybe "[]" body) ] (cs $ fromMaybe "[]" body)
@@ -119,8 +119,7 @@ app conf reqBody req =
encode . object $ [("message", String "Failed to parse user.")] encode . object $ [("message", String "Failed to parse user.")]
Just u -> do Just u -> do
setRole authenticator setRole authenticator
login <- signInRole (cs $ userId u) login <- signInRole (cs $ userId u) (cs $ userPass u)
(cs $ userPass u)
case login of case login of
LoginSuccess role uid -> LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $ return $ responseLBS status201 [ jsonH ] $
@@ -133,15 +132,15 @@ app conf reqBody req =
echoRequested = lookupHeader "Prefer" == Just "return=representation" echoRequested = lookupHeader "Prefer" == Just "return=representation"
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value)) parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
parsed = if lookupHeader "Content-Type" == Just csvMT parsed = if lookupHeader "Content-Type" == Just csvMT
then do then do
rows <- CSV.decode CSV.NoHeader reqBody rows <- CSV.decode CSV.NoHeader reqBody
if V.null rows then Left "CSV requires header" if V.null rows then Left "CSV requires header"
else Right (V.head rows, (V.map $ V.map $ parseCsvCell . cs) (V.tail rows)) else Right (V.head rows, (V.map $ V.map $ parseCsvCell . cs) (V.tail rows))
else eitherDecode reqBody >>= \val -> else eitherDecode reqBody >>= \val ->
case val of case val of
Object obj -> Right . second V.singleton . V.unzip . V.fromList $ Object obj -> Right . second V.singleton . V.unzip . V.fromList $
M.toList obj M.toList obj
_ -> Left "Expecting single JSON object or CSV rows" _ -> Left "Expecting single JSON object or CSV rows"
case parsed of case parsed of
Left err -> return $ responseLBS status400 [] $ Left err -> return $ responseLBS status400 [] $
encode . object $ [("message", String $ "Failed to parse JSON payload. " <> cs err)] encode . object $ [("message", String $ "Failed to parse JSON payload. " <> cs err)]
@@ -186,7 +185,7 @@ app conf reqBody req =
let specifiedKeys = map (cs . fst) qq let specifiedKeys = map (cs . fst) qq
if S.fromList primaryKeys /= S.fromList specifiedKeys if S.fromList primaryKeys /= S.fromList specifiedKeys
then return $ responseLBS status405 [] then return $ responseLBS status405 []
"You must speficy all and only primary keys as params" "You must speficy all and only primary keys as params"
else do else do
tableCols <- map (cs . colName) <$> columns qt tableCols <- map (cs . colName) <$> columns qt
let cols = map cs $ M.keys obj let cols = map cs $ M.keys obj
@@ -194,21 +193,21 @@ app conf reqBody req =
then do then do
let vals = M.elems obj let vals = M.elems obj
H.unitEx $ iffNotT H.unitEx $ iffNotT
(whereT qt qq $ update qt cols vals) (whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals) (insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] "" return $ responseLBS status204 [ jsonH ] ""
else return $ if Prelude.null tableCols else return $ if Prelude.null tableCols
then responseLBS status404 [] "" then responseLBS status404 [] ""
else responseLBS status400 [] else responseLBS status400 []
"You must specify all columns in PUT request" "You must specify all columns in PUT request"
([table], "PATCH") -> ([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do handleJsonObj reqBody $ \obj -> do
let qt = qualify table let qt = qualify table
up = returningStarT up = returningStarT
. whereT qt qq . whereT qt qq
$ update qt (map cs $ M.keys obj) (M.elems obj) $ update qt (map cs $ M.keys obj) (M.elems obj)
patch = withT up "t" $ B.Stmt patch = withT up "t" $ B.Stmt
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying" "select count(t), array_to_json(array_agg(row_to_json(t)))::character varying"
V.empty True V.empty True
@@ -232,8 +231,8 @@ app conf reqBody req =
row <- H.maybeEx del row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
return $ if deletedCount == 0 return $ if deletedCount == 0
then responseLBS status404 [] "" then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] "" else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] ""
(_, _) -> (_, _) ->
return $ responseLBS status404 [] "" return $ responseLBS status404 [] ""
@@ -271,19 +270,20 @@ contentRangeH from to total =
("Content-Range", ("Content-Range",
if total == 0 || from > total if total == 0 || from > total
then "*/" <> cs (show total) then "*/" <> cs (show total)
else cs (show from) <> "-" else cs (show from)
<> cs (show to) <> "/" <> "-" <> cs (show to)
<> cs (show total) <> "/" <> cs (show total)
) )
requestedSchema :: Text -> Maybe BS.ByteString -> Text requestedSchema :: Text -> Maybe BS.ByteString -> Text
requestedSchema v1schema accept = requestedSchema v1schema accept =
case verStr of case verStr of
Just [[_, ver]] -> if ver == "1" then v1schema else cs ver Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
_ -> v1schema _ -> v1schema
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString where
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]] verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
jsonMT :: BS.ByteString jsonMT :: BS.ByteString
@@ -292,19 +292,21 @@ jsonMT = "application/json"
csvMT :: BS.ByteString csvMT :: BS.ByteString
csvMT = "text/csv" csvMT = "text/csv"
allMT :: BS.ByteString
allMT = "*/*"
jsonH :: Header jsonH :: Header
jsonH = (hContentType, jsonMT) jsonH = (hContentType, jsonMT)
contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString
contentTypeForAccept accept contentTypeForAccept accept
| isNothing accept || hasJson = Just jsonMT | isNothing accept || has allMT || has jsonMT = Just jsonMT
| hasCsv = Just csvMT | has csvMT = Just csvMT
| otherwise = Nothing | otherwise = Nothing
where where
Just acceptH = accept Just acceptH = accept
findInAccept = flip find $ parseHttpAccept acceptH findInAccept = flip find $ parseHttpAccept acceptH
hasJson = isJust $ findInAccept $ BS.isPrefixOf jsonMT has = isJust . findInAccept . BS.isPrefixOf
hasCsv = isJust $ findInAccept $ BS.isPrefixOf csvMT
bodyForAccept :: BS.ByteString -> QualifiedIdentifier -> StatementT bodyForAccept :: BS.ByteString -> QualifiedIdentifier -> StatementT
bodyForAccept contentType table bodyForAccept contentType table
+4 -4
View File
@@ -56,10 +56,10 @@ setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True
setUserId :: Text -> H.Tx P.Postgres s () setUserId :: Text -> H.Tx P.Postgres s ()
setUserId uid = if uid /= "" then setUserId uid =
H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True if uid /= ""
else then H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
resetUserId else resetUserId
resetUserId :: H.Tx P.Postgres s () resetUserId :: H.Tx P.Postgres s ()
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|] resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
+4 -4
View File
@@ -22,7 +22,7 @@ data AppConfig = AppConfig {
, configSecure :: Bool , configSecure :: Bool
, configPool :: Int , configPool :: Int
, configV1Schema :: String , configV1Schema :: String
, configJwtSecret :: String , configJwtSecret :: String
} }
@@ -52,9 +52,9 @@ corsPolicy req = case lookup "origin" headers of
corsOrigins = Just ([origin], True) corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders , corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [ , corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type" "Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit" , "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
] ]
} }
Nothing -> Nothing Nothing -> Nothing
where where
+13 -10
View File
@@ -36,12 +36,12 @@ main = do
hSetBuffering stderr NoBuffering hSetBuffering stderr NoBuffering
let opts = info (helper <*> argParser) $ let opts = info (helper <*> argParser) $
fullDesc fullDesc
<> progDesc ( <> progDesc (
"PostgREST " "PostgREST "
<> prettyVersion <> prettyVersion
<> " / create a REST API to an existing Postgres database" <> " / create a REST API to an existing Postgres database"
) )
parserPrefs = prefs showHelpOnError parserPrefs = prefs showHelpOnError
conf <- customExecParser parserPrefs opts conf <- customExecParser parserPrefs opts
let port = configPort conf let port = configPort conf
@@ -64,12 +64,15 @@ main = do
middle = logStdout . defaultMiddle (configSecure conf) middle = logStdout . defaultMiddle (configSecure conf)
poolSettings <- maybe (fail "Improper session settings") return $ poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30 H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres pool :: H.Pool P.Postgres <- H.acquirePool pgSettings poolSettings
<- H.acquirePool pgSettings poolSettings
resOrError <- H.session pool isServerVersionSupported resOrError <- H.session pool isServerVersionSupported
either (fail . show) (\supported -> unless supported $ fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0") resOrError either (fail . show)
(\supported ->
unless supported $
fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0"
) resOrError
runSettings appSettings $ middle $ \req respond -> do runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req body <- strictRequestBody req
+9 -9
View File
@@ -78,12 +78,12 @@ redirectInsecure app req respond = do
if not (isSecure req || isHerokuSecure) if not (isSecure req || isHerokuSecure)
then case uriM of then case uriM of
Just uri -> Just uri ->
respond $ responseLBS status301 [ respond $ responseLBS status301 [
(hLocation, cs . show $ uri { uriScheme = "https:" }) (hLocation, cs . show $ uri { uriScheme = "https:" })
] "" ] ""
Nothing -> Nothing ->
respond $ responseLBS status400 [] "SSL is required" respond $ responseLBS status400 [] "SSL is required"
else app req respond else app req respond
unsupportedAccept :: Application -> Application unsupportedAccept :: Application -> Application
@@ -96,6 +96,6 @@ unsupportedAccept app req respond = do
defaultMiddle :: Bool -> Application -> Application defaultMiddle :: Bool -> Application -> Application
defaultMiddle secure = (if secure then redirectInsecure else id) defaultMiddle secure = (if secure then redirectInsecure else id)
. gzip def . cors corsPolicy . gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")]) . staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept . unsupportedAccept
+78 -72
View File
@@ -54,13 +54,13 @@ limitT r q =
whereT :: QualifiedIdentifier -> Net.Query -> StatementT whereT :: QualifiedIdentifier -> Net.Query -> StatementT
whereT table params q = whereT table params q =
if L.null cols if L.null cols
then q then q
else q <> B.Stmt " where " empty True <> conjunction else q <> B.Stmt " where " empty True <> conjunction
where where
cols = [ col | col <- params, fst col `notElem` ["order","select"] ] cols = [ col | col <- params, fst col `notElem` ["order","select"] ]
wherePredTable = wherePred table wherePredTable = wherePred table
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols) conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
withT :: PStmt -> T.Text -> StatementT withT :: PStmt -> T.Text -> StatementT
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) = withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) =
@@ -74,13 +74,13 @@ orderT ts q =
then q then q
else q <> B.Stmt " order by " empty True <> clause else q <> B.Stmt " order by " empty True <> clause
where where
clause = mconcat $ L.intersperse commaq (map queryTerm ts) clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> PStmt queryTerm :: OrderTerm -> PStmt
queryTerm t = B.Stmt queryTerm t = B.Stmt
(" " <> cs (pgFmtIdent $ otTerm t) <> " " (" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> cs (otDirection t) <> " " <> cs (otDirection t) <> " "
<> maybe "" cs (otNullOrder t) <> " ") <> maybe "" cs (otNullOrder t) <> " ")
empty True empty True
parentheticT :: StatementT parentheticT :: StatementT
parentheticT s = parentheticT s =
@@ -105,7 +105,8 @@ asCsvWithCount :: QualifiedIdentifier -> StatementT
asCsvWithCount table = withCount . asCsv table asCsvWithCount table = withCount . asCsv table
asCsv :: QualifiedIdentifier -> StatementT asCsv :: QualifiedIdentifier -> StatementT
asCsv table s = s { B.stmtTemplate = asCsv table s = s {
B.stmtTemplate =
"(select string_agg(quote_ident(column_name::text), ',') from " "(select string_agg(quote_ident(column_name::text), ',') from "
<> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '" <> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '"
<> fromQi table <> "' order by ordinal_position) h) || '\r' || " <> fromQi table <> "' order by ordinal_position) h) || '\r' || "
@@ -116,7 +117,8 @@ asJsonWithCount :: StatementT
asJsonWithCount = withCount . asJson asJsonWithCount = withCount . asJson
asJson :: StatementT asJson :: StatementT
asJson s = s { B.stmtTemplate = asJson s = s {
B.stmtTemplate =
"array_to_json(array_agg(row_to_json(t)))::character varying from (" "array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" } <> B.stmtTemplate s <> ") t" }
@@ -131,26 +133,30 @@ selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
select :: QualifiedIdentifier -> Net.Query -> PStmt select :: QualifiedIdentifier -> Net.Query -> PStmt
select table params = select table params =
if L.null cols if L.null cols
then selectStar table then selectStar table
else B.Stmt "select " empty True <> conjunction <> B.Stmt (" from " <> fromQi table ) empty True else B.Stmt "select " empty True <> conjunction <> B.Stmt (" from " <> fromQi table ) empty True
where where
selectTermTable = selectTerm table selectTermTable = selectTerm table
conjunction = mconcat $ L.intersperse commaq (map selectTermTable cols) conjunction = mconcat $ L.intersperse commaq (map selectTermTable cols)
columnsParam = fromMaybe "" $ join (lookup "select" params) columnsParam = fromMaybe "" $ join (lookup "select" params)
cols = filter ((>0) . T.length) $ map T.strip $ T.split (==',') $ cs columnsParam cols = filter ((>0) . T.length) $ map T.strip $ T.split (==',') $ cs columnsParam
selectTerm :: QualifiedIdentifier -> T.Text -> PStmt selectTerm :: QualifiedIdentifier -> T.Text -> PStmt
selectTerm table col = selectTerm table col =
case T.splitOn "::" col of case T.splitOn "::" col of
[colName,castTo] -> B.Stmt ("CAST (" <> pgFmtJsonbPath table (cs colName) <> " AS " <> castToSafe <> " )" <> asT (jsonbPath colName)) empty True [colName,castTo] ->
where castToSafe = T.filter ( `elem` ['a'..'z'] ) castTo B.Stmt (
_-> B.Stmt (pgFmtJsonbPath table (cs col) <> asT (jsonbPath col)) empty True "CAST (" <> pgFmtJsonbPath table (cs colName) <> " AS "
where <> castToSafe <> " )" <> asT (jsonbPath colName)
jsonbPath :: T.Text -> Maybe JsonbPath ) empty True
jsonbPath c = parseJsonbPath $ cs c where castToSafe = T.filter ( `elem` ['a'..'z'] ) castTo
asT (Just (DoubleArrow _ (KeyIdentifier key))) = " AS " <> pgFmtIdent key _ -> B.Stmt (pgFmtJsonbPath table (cs col) <> asT (jsonbPath col)) empty True
asT _ = "" where
jsonbPath :: T.Text -> Maybe JsonbPath
jsonbPath c = parseJsonbPath $ cs c
asT (Just (DoubleArrow _ (KeyIdentifier key))) = " AS " <> pgFmtIdent key
asT _ = ""
returningStarT :: StatementT returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" } returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
@@ -212,39 +218,39 @@ wherePred table (col, predicate) =
where where
headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate "" notOp = hasNot headPredicate ""
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest) value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
whiteList val = fromMaybe (cs (pgFmtLit val) <> "::unknown ") whiteList val = fromMaybe
(L.find ((==) . T.toLower $ val) (cs (pgFmtLit val) <> "::unknown ")
["null","true","false"]) (L.find ((==) . T.toLower $ val) ["null","true","false"])
star c = if c == '*' then '%' else c star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit unknownLiteral = (<> "::unknown ") . pgFmtLit
sqlValue = case opCode of sqlValue = case opCode of
"like" -> unknownLiteral $ T.map star value "like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value "ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") " "in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") " "notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") " "@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value _ -> unknownLiteral value
op = case opCode of op = case opCode of
"eq" -> "=" "eq" -> "="
"gt" -> ">" "gt" -> ">"
"lt" -> "<" "lt" -> "<"
"gte" -> ">=" "gte" -> ">="
"lte" -> "<=" "lte" -> "<="
"neq" -> "<>" "neq" -> "<>"
"like"-> "like" "like"-> "like"
"ilike"-> "ilike" "ilike"-> "ilike"
"in" -> "in" "in" -> "in"
"notin" -> "not in" "notin" -> "not in"
"is" -> "is" "is" -> "is"
"isnot" -> "is not" "isnot" -> "is not"
"@@" -> "@@" "@@" -> "@@"
_ -> "=" _ -> "="
orderParse :: Net.Query -> [OrderTerm] orderParse :: Net.Query -> [OrderTerm]
orderParse q = orderParse q =
@@ -255,18 +261,18 @@ orderParse q =
orderParseTerm :: T.Text -> Maybe OrderTerm orderParseTerm :: T.Text -> Maybe OrderTerm
orderParseTerm s = orderParseTerm s =
case T.split (=='.') s of case T.split (=='.') s of
(c:d:nls) -> (c:d:nls) ->
if d `elem` ["asc", "desc"] if d `elem` ["asc", "desc"]
then Just $ OrderTerm c then Just $ OrderTerm c
( if d == "asc" then "asc" else "desc" ) ( if d == "asc" then "asc" else "desc" )
( case nls of ( case nls of
[n] -> if | n == "nullsfirst" -> Just "nulls first" [n] -> if | n == "nullsfirst" -> Just "nulls first"
| n == "nullslast" -> Just "nulls last" | n == "nullslast" -> Just "nulls last"
| otherwise -> Nothing | otherwise -> Nothing
_ -> Nothing _ -> Nothing
) )
else Nothing else Nothing
_ -> Nothing _ -> Nothing
commaq :: PStmt commaq :: PStmt
commaq = B.Stmt ", " empty True commaq = B.Stmt ", " empty True
+38 -37
View File
@@ -43,8 +43,8 @@ tables :: Text -> H.Tx P.Postgres s [Table]
tables schema = do tables schema = do
rows <- H.listEx $ rows <- H.listEx $
[H.stmt| [H.stmt|
select select
n.nspname as table_schema, n.nspname as table_schema,
relname as table_name, relname as table_name,
c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8 c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
or (exists ( or (exists (
@@ -52,16 +52,17 @@ tables schema = do
from pg_trigger from pg_trigger
where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69) where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69)
) as insertable ) as insertable
from from
pg_class c pg_class c
join pg_namespace n on n.oid = c.relnamespace join pg_namespace n on n.oid = c.relnamespace
where where
c.relkind in ('v', 'r', 'm') c.relkind in ('v', 'r', 'm')
and n.nspname = ? and n.nspname = ?
and ( and (
pg_has_role(c.relowner, 'USAGE'::text) pg_has_role(c.relowner, 'USAGE'::text)
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text)
) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
)
order by relname order by relname
|] schema |] schema
return $ map tableFromRow rows return $ map tableFromRow rows
@@ -71,31 +72,31 @@ columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column]
columns table = do columns table = do
cols <- H.listEx $ [H.stmt| cols <- H.listEx $ [H.stmt|
select info.table_schema as schema, info.table_name as table_name, select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position, info.column_name as name, info.ordinal_position as position,
info.is_nullable::boolean as nullable, info.data_type as col_type, info.is_nullable::boolean as nullable, info.data_type as col_type,
info.is_updatable::boolean as updatable, info.is_updatable::boolean as updatable,
info.character_maximum_length as max_len, info.character_maximum_length as max_len,
info.numeric_precision as precision, info.numeric_precision as precision,
info.column_default as default_value, info.column_default as default_value,
array_to_string(enum_info.vals, ',') as enum array_to_string(enum_info.vals, ',') as enum
from ( from (
select table_schema, table_name, column_name, ordinal_position, select table_schema, table_name, column_name, ordinal_position,
is_nullable, data_type, is_updatable, is_nullable, data_type, is_updatable,
character_maximum_length, numeric_precision, character_maximum_length, numeric_precision,
column_default, udt_name column_default, udt_name
from information_schema.columns from information_schema.columns
where table_schema = ? and table_name = ? where table_schema = ? and table_name = ?
) as info ) as info
left outer join ( left outer join (
select n.nspname as s, select n.nspname as s,
t.typname as n, t.typname as n,
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
from pg_type t from pg_type t
join pg_enum e on t.oid = e.enumtypid join pg_enum e on t.oid = e.enumtypid
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
group by s, n group by s, n
) as enum_info ) as enum_info
on (info.udt_name = enum_info.n) on (info.udt_name = enum_info.n)
order by position |] order by position |]
(qiSchema table) (qiName table) (qiSchema table) (qiName table)
+6 -6
View File
@@ -41,15 +41,15 @@ rangeRequested = (rangeParse =<<) . lookup hRange
rangeLimit :: NonnegRange -> Maybe Int rangeLimit :: NonnegRange -> Maybe Int
rangeLimit range = rangeLimit range =
case [rangeLower range, rangeUpper range] case [rangeLower range, rangeUpper range] of
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from) [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing _ -> Nothing
rangeOffset :: NonnegRange -> Int rangeOffset :: NonnegRange -> Int
rangeOffset range = rangeOffset range =
case rangeLower range case rangeLower range of
of BoundaryBelow from -> from BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen _ -> error "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange rangeGeq :: Int -> NonnegRange
rangeGeq n = rangeGeq n =
+6
View File
@@ -261,6 +261,12 @@ spec = afterAll_ resetDb $ around withApp $ do
liftIO $ simpleHeaders g liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10" `shouldSatisfy` matchHeader "Content-Range" "0-9/10"
it "can set a column to NULL" $ do
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |]
get "/no_pk?a=eq.keepme" `shouldRespondWith`
[json| [{ a: "keepme", b: null }] |]
it "can update based on a computed column" $ it "can update based on a computed column" $
request methodPatch request methodPatch
"/items?always_true=eq.false" "/items?always_true=eq.false"
+5
View File
@@ -234,6 +234,11 @@ spec =
(acceptHdrs "text/unknowntype") "" (acceptHdrs "text/unknowntype") ""
`shouldRespondWith` 415 `shouldRespondWith` 415
it "should respond correctly to */* in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "*/*") ""
`shouldRespondWith` 200
it "should respond correctly to multiple types in accept header" $ it "should respond correctly to multiple types in accept header" $
request methodGet "/simple_pk" request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype, text/csv") "" (acceptHdrs "text/unknowntype, text/csv") ""