Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6769ea16 | ||
|
|
1e417aef90 | ||
|
|
60eb826cb0 | ||
|
|
3d3c7277c7 | ||
|
|
5be0b5d5ae | ||
|
|
11e5918690 | ||
|
|
c207e7ee30 | ||
|
|
6474a83221 | ||
|
|
9b15071f23 | ||
|
|
51b379d78b | ||
|
|
cac5daca01 | ||
|
|
4bc66c4ca3 |
@@ -3,6 +3,13 @@
|
|||||||
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/).
|
||||||
|
|
||||||
|
## [0.2.7.0] - 2015-03-03
|
||||||
|
### Added
|
||||||
|
- Server response logging
|
||||||
|
- Filter IN values, e.g. `?col=in.1,2,3`
|
||||||
|
- Return POSTed resource if header "Prefer: return=representation"
|
||||||
|
- Allow override of default (v1) schema
|
||||||
|
|
||||||
## [0.2.6.0] - 2015-02-18
|
## [0.2.6.0] - 2015-02-18
|
||||||
### Added
|
### Added
|
||||||
- A changelog
|
- A changelog
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ PostgREST serves a fully RESTful API from any existing PostgreSQL
|
|||||||
database. It provides a cleaner, more standards-compliant, faster
|
database. It provides a cleaner, more standards-compliant, faster
|
||||||
API than you are likely to write from scratch.
|
API than you are likely to write from scratch.
|
||||||
|
|
||||||
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](https://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
|
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
|
||||||
|
|
||||||
Try making requests to the live demo server with an HTTP client
|
Try making requests to the live demo server with an HTTP client
|
||||||
such as [postman](http://www.getpostman.com/). The structure of the
|
such as [postman](http://www.getpostman.com/). The structure of the
|
||||||
@@ -20,16 +20,21 @@ your own projects.
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.6.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.6.0.tar.xz)) and invoke like so:
|
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.7.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.7.0.tar.xz)) and invoke like so:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
postgrest --db-host localhost --db-port 5432 \
|
postgrest --db-host localhost --db-port 5432 \
|
||||||
--db-name my_db --db-user postgres \
|
--db-name my_db --db-user postgres \
|
||||||
--db-pass foobar --db-pool 200 \
|
--db-pass foobar --db-pool 200 \
|
||||||
--anonymous postgres --secure \
|
--anonymous postgres --port 3000 \
|
||||||
--port 3000
|
--v1schema public
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In production include the `--secure` option which redirects all
|
||||||
|
requests to HTTPS. Note that PostgREST does not handle the SSL
|
||||||
|
internally and must be put behind another server that does (such
|
||||||
|
as nginx or the Heroku load balancer).
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
|
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"POSTGREST_VER": {
|
"POSTGREST_VER": {
|
||||||
"description": "Version of PostgREST to deploy",
|
"description": "Version of PostgREST to deploy",
|
||||||
"value": "0.2.6.0"
|
"value": "0.2.7.0"
|
||||||
},
|
},
|
||||||
"DB_NAME": {
|
"DB_NAME": {
|
||||||
"description": "Database name",
|
"description": "Database name",
|
||||||
|
|||||||
+1
-1
@@ -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.6.0
|
version: 0.2.7.0
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
|
|||||||
+9
-8
@@ -35,8 +35,8 @@ import PgQuery
|
|||||||
import RangeQuery
|
import RangeQuery
|
||||||
import PgStructure
|
import PgStructure
|
||||||
|
|
||||||
app :: BL.ByteString -> Request -> H.Tx P.Postgres s Response
|
app :: Text -> BL.ByteString -> Request -> H.Tx P.Postgres s Response
|
||||||
app reqBody req =
|
app v1schema reqBody req =
|
||||||
case (path, verb) of
|
case (path, verb) of
|
||||||
([], _) -> do
|
([], _) -> do
|
||||||
body <- encode <$> tables (cs schema)
|
body <- encode <$> tables (cs schema)
|
||||||
@@ -102,6 +102,7 @@ app reqBody req =
|
|||||||
handleJsonObj reqBody $ \obj -> do
|
handleJsonObj reqBody $ \obj -> do
|
||||||
let qt = QualifiedTable schema (cs table)
|
let qt = QualifiedTable schema (cs table)
|
||||||
query = insertInto qt (map cs $ keys obj) (elems obj)
|
query = insertInto qt (map cs $ keys obj) (elems obj)
|
||||||
|
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
||||||
row <- H.maybeEx query
|
row <- H.maybeEx query
|
||||||
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
|
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
|
||||||
Just inserted = decode (cs insertedJson) :: Maybe Object
|
Just inserted = decode (cs insertedJson) :: Maybe Object
|
||||||
@@ -116,7 +117,7 @@ app reqBody req =
|
|||||||
return $ responseLBS status201
|
return $ responseLBS status201
|
||||||
[ jsonH
|
[ jsonH
|
||||||
, (hLocation, "/" <> cs table <> "?" <> cs params)
|
, (hLocation, "/" <> cs table <> "?" <> cs params)
|
||||||
] ""
|
] $ if echoRequested then cs insertedJson else ""
|
||||||
|
|
||||||
([table], "PUT") ->
|
([table], "PUT") ->
|
||||||
handleJsonObj reqBody $ \obj -> do
|
handleJsonObj reqBody $ \obj -> do
|
||||||
@@ -170,7 +171,7 @@ app reqBody req =
|
|||||||
verb = requestMethod req
|
verb = requestMethod req
|
||||||
qq = queryString req
|
qq = queryString req
|
||||||
hdrs = requestHeaders req
|
hdrs = requestHeaders req
|
||||||
schema = requestedSchema hdrs
|
schema = requestedSchema v1schema hdrs
|
||||||
range = rangeRequested hdrs
|
range = rangeRequested hdrs
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
|
|
||||||
@@ -196,11 +197,11 @@ contentRangeH from to total =
|
|||||||
<> cs (show total)
|
<> cs (show total)
|
||||||
)
|
)
|
||||||
|
|
||||||
requestedSchema :: RequestHeaders -> Text
|
requestedSchema :: Text -> RequestHeaders -> Text
|
||||||
requestedSchema hdrs =
|
requestedSchema v1schema hdrs =
|
||||||
case verStr of
|
case verStr of
|
||||||
Just [[_, ver]] -> ver
|
Just [[_, ver]] -> if ver == "1" then v1schema else ver
|
||||||
_ -> "1"
|
_ -> v1schema
|
||||||
|
|
||||||
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
|
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
|
||||||
accept = cs <$> lookup hAccept hdrs :: Maybe Text
|
accept = cs <$> lookup hAccept hdrs :: Maybe Text
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ data AppConfig = AppConfig {
|
|||||||
, configAnonRole :: String
|
, configAnonRole :: String
|
||||||
, configSecure :: Bool
|
, configSecure :: Bool
|
||||||
, configPool :: Int
|
, configPool :: Int
|
||||||
|
, configV1Schema :: String
|
||||||
}
|
}
|
||||||
|
|
||||||
argParser :: Parser AppConfig
|
argParser :: Parser AppConfig
|
||||||
@@ -34,6 +35,7 @@ argParser = AppConfig
|
|||||||
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
|
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
|
||||||
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
|
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
|
||||||
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
|
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
|
||||||
|
<*> strOption (long "v1schema" <> metavar "NAME" <> value "1" <> help "Schema to use for nonspecified version (or explicit v1)" <> showDefault)
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||||
|
|||||||
+4
-3
@@ -14,6 +14,7 @@ import Network.Wai.Middleware.Cors (cors)
|
|||||||
import Network.Wai.Handler.Warp hiding (Connection)
|
import Network.Wai.Handler.Warp hiding (Connection)
|
||||||
import Network.Wai.Middleware.Gzip (gzip, def)
|
import Network.Wai.Middleware.Gzip (gzip, def)
|
||||||
import Network.Wai.Middleware.Static (staticPolicy, only)
|
import Network.Wai.Middleware.Static (staticPolicy, only)
|
||||||
|
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (versionBranch)
|
||||||
import qualified Hasql as H
|
import qualified Hasql as H
|
||||||
@@ -47,8 +48,8 @@ main = do
|
|||||||
appSettings = setPort port
|
appSettings = setPort port
|
||||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||||
$ defaultSettings
|
$ defaultSettings
|
||||||
middle =
|
middle = logStdout
|
||||||
(if configSecure conf then redirectInsecure else id)
|
. (if configSecure conf 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")])
|
||||||
anonRole = cs $ configAnonRole conf
|
anonRole = cs $ configAnonRole conf
|
||||||
@@ -62,7 +63,7 @@ main = do
|
|||||||
runSettings appSettings $ middle $ \req respond -> do
|
runSettings appSettings $ middle $ \req respond -> do
|
||||||
body <- strictRequestBody req
|
body <- strictRequestBody req
|
||||||
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
|
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
|
||||||
authenticated currRole anonRole (app body) req
|
authenticated currRole anonRole (app (cs $ configV1Schema conf) body) req
|
||||||
either (respond . errResponse) respond resOrError
|
either (respond . errResponse) respond resOrError
|
||||||
|
|
||||||
where
|
where
|
||||||
|
|||||||
+12
-4
@@ -138,15 +138,22 @@ update t cols vals = B.Stmt
|
|||||||
|
|
||||||
wherePred :: Net.QueryItem -> PStmt
|
wherePred :: Net.QueryItem -> PStmt
|
||||||
wherePred (col, predicate) = B.Stmt
|
wherePred (col, predicate) = B.Stmt
|
||||||
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ")
|
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs sqlValue)
|
||||||
empty True
|
empty True
|
||||||
|
|
||||||
where
|
where
|
||||||
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
||||||
unStarredVal = T.intercalate "." rest
|
value = T.intercalate "." rest
|
||||||
|
|
||||||
star c = if c == '*' then '%' else c
|
star c = if c == '*' then '%' else c
|
||||||
value = if opCode == "like" || opCode == "ilike"
|
unknownLiteral = (<> "::unknown ") . pgFmtLit
|
||||||
then T.map star unStarredVal else unStarredVal
|
|
||||||
|
sqlValue = case opCode of
|
||||||
|
"like" -> unknownLiteral $ T.map star value
|
||||||
|
"ilike" -> unknownLiteral $ T.map star value
|
||||||
|
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
|
||||||
|
_ -> unknownLiteral value
|
||||||
|
|
||||||
op = case opCode of
|
op = case opCode of
|
||||||
"eq" -> "="
|
"eq" -> "="
|
||||||
"gt" -> ">"
|
"gt" -> ">"
|
||||||
@@ -156,6 +163,7 @@ wherePred (col, predicate) = B.Stmt
|
|||||||
"neq" -> "<>"
|
"neq" -> "<>"
|
||||||
"like"-> "like"
|
"like"-> "like"
|
||||||
"ilike"-> "ilike"
|
"ilike"-> "ilike"
|
||||||
|
"in" -> "in"
|
||||||
_ -> "="
|
_ -> "="
|
||||||
|
|
||||||
orderParse :: Net.Query -> [OrderTerm]
|
orderParse :: Net.Query -> [OrderTerm]
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ spec = afterAll_ resetDb $ around withApp $ do
|
|||||||
post "/simple_pk" [json| { "extra":"foo"} |]
|
post "/simple_pk" [json| { "extra":"foo"} |]
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
context "into a table with no pk" . after_ (clearTable "no_pk") $
|
context "into a table with no pk" . after_ (clearTable "no_pk") $ do
|
||||||
it "succeeds with 201 and a link including all fields" $ do
|
it "succeeds with 201 and a link including all fields" $ do
|
||||||
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
|
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
@@ -57,6 +57,15 @@ spec = afterAll_ resetDb $ around withApp $ do
|
|||||||
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
|
||||||
simpleStatus p `shouldBe` created201
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
|
it "returns full details of inserted record if asked" $ do
|
||||||
|
p <- request methodPost "/no_pk"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| { "a":"bar", "b":"baz" } |]
|
||||||
|
liftIO $ do
|
||||||
|
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
|
||||||
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
|
||||||
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
|
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
|
||||||
it "builds response location header appropriately" $
|
it "builds response location header appropriately" $
|
||||||
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
|
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "matches items IN" $
|
||||||
|
get "/items?id=in.1,3,5"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||||
|
}
|
||||||
|
|
||||||
it "matches with like" $ do
|
it "matches with like" $ do
|
||||||
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
|
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
|
||||||
[{"a":"xyyx","b":"u"}]|]
|
[{"a":"xyyx","b":"u"}]|]
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ isLeft (Left _ ) = True
|
|||||||
isLeft _ = False
|
isLeft _ = False
|
||||||
|
|
||||||
cfg :: AppConfig
|
cfg :: AppConfig
|
||||||
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
|
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10 "1"
|
||||||
|
|
||||||
testPoolOpts :: PoolSettings
|
testPoolOpts :: PoolSettings
|
||||||
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
|
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
|
||||||
@@ -57,7 +57,7 @@ withApp perform = do
|
|||||||
perform $ middle $ \req resp -> do
|
perform $ middle $ \req resp -> do
|
||||||
body <- strictRequestBody req
|
body <- strictRequestBody req
|
||||||
result <- liftIO $ H.session pool $ H.tx Nothing
|
result <- liftIO $ H.session pool $ H.tx Nothing
|
||||||
$ authenticated currRole anonRole (app body) req
|
$ authenticated currRole anonRole (app (cs $ configV1Schema cfg) body) req
|
||||||
either (resp . errResponse) resp result
|
either (resp . errResponse) resp result
|
||||||
|
|
||||||
where middle = cors corsPolicy
|
where middle = cors corsPolicy
|
||||||
|
|||||||
Reference in New Issue
Block a user