Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
713b214c9a | ||
|
|
681388631b | ||
|
|
ae9e27a0c7 | ||
|
|
e83144ce7f | ||
|
|
1c54c7130a | ||
|
|
1a8d5fed8a | ||
|
|
57ebf43e85 | ||
|
|
b87734343e | ||
|
|
c80c9ef726 | ||
|
|
d5758523f3 | ||
|
|
ee40e7e0d7 | ||
|
|
53b606e1c1 |
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: postgrest
|
||||
@@ -1,3 +1,7 @@
|
||||
<!--
|
||||
Before reporting a bug:
|
||||
If your database schema has changed while the PostgREST server is running, send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/v5.2/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||
-->
|
||||
### Environment
|
||||
|
||||
* PostgreSQL version: (if using docker, specify the image)
|
||||
|
||||
+3
-1
@@ -29,16 +29,18 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
## Lead Backers
|
||||
|
||||
- [Daniel Babiak](https://github.com/d-babiak)
|
||||
- [Michel Pelletier](https://github.com/michelp/)
|
||||
- Evans Fernandes
|
||||
|
||||
## Backers
|
||||
|
||||
- Tsingson Qin
|
||||
- Michel Pelletier
|
||||
- Jay Hannah
|
||||
- Robert Stolarz
|
||||
- Kofi Gumbs
|
||||
- Nicholas DiBiase
|
||||
- Christopher Reid
|
||||
- Nathan Bouscal
|
||||
|
||||
## Former Backers
|
||||
|
||||
|
||||
@@ -9,6 +9,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Fixed
|
||||
|
||||
## [6.0.2] - 2019-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1369, Change `raw-media-types` to accept a string of comma separated MIME types - @Dansvidania
|
||||
- #1368, Fix long column descriptions being truncated at 63 characters in PostgreSQL 12 - @amedeedaboville
|
||||
- #1348, Go back to converting plus "+" to space " " in querystrings by default - @steve-chavez
|
||||
|
||||
### Deprecated
|
||||
|
||||
- #1348, Deprecate `.` symbol for disambiguating resource embedding(added in #918). The url-safe '!' should be used instead. We refrained from using `+` as part of our syntax because it conflicts with some http clients and proxies.
|
||||
|
||||
## [6.0.1] - 2019-07-30
|
||||
|
||||
### Added
|
||||
|
||||
@@ -50,13 +50,15 @@ Big thanks to our sponsors! You can join them by supporting PostgREST on [Patreo
|
||||
```bash
|
||||
postgrest --help
|
||||
```
|
||||
## [Documentation](http://postgrest.org)
|
||||
|
||||
Latest documentation is at [postgrest.org](http://postgrest.org). You can contribute to the docs in [PostgREST/postgrest-docs](https://github.com/PostgREST/postgrest-docs).
|
||||
|
||||
## Performance
|
||||
|
||||
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
|
||||
free tier. If you're used to servers written in interpreted languages
|
||||
(or named after precious gems), prepare to be pleasantly surprised by
|
||||
PostgREST performance.
|
||||
free tier. If you're used to servers written in interpreted languages,
|
||||
prepare to be pleasantly surprised by PostgREST performance.
|
||||
|
||||
Three factors contribute to the speed. First the server is written
|
||||
in [Haskell](https://www.haskell.org/) using the
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"POSTGREST_VER": {
|
||||
"description": "Version of PostgREST to deploy",
|
||||
"value": "6.0.1"
|
||||
"value": "6.0.2"
|
||||
},
|
||||
"DB_URI": {
|
||||
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
|
||||
|
||||
@@ -16,5 +16,4 @@ role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
|
||||
max-rows = "$(PGRST_MAX_ROWS)"
|
||||
pre-request = "$(PGRST_PRE_REQUEST)"
|
||||
root-spec = "$(PGRST_ROOT_SPEC)"
|
||||
|
||||
raw-media-types = "$(PGRST_RAW_MEDIA_TYPES)"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 6.0.1
|
||||
version: 6.0.2
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for the tables and views, supporting all HTTP verbs that security
|
||||
|
||||
@@ -138,14 +138,14 @@ userApiRequest schema rootSpec req reqBody
|
||||
, iCanonicalQS = toS $ urlEncodeVars
|
||||
. L.sortOn fst
|
||||
. map (join (***) toS . second (fromMaybe BS.empty))
|
||||
$ queryStringWPlus
|
||||
$ qString
|
||||
, iJWT = tokenStr
|
||||
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie]
|
||||
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
|
||||
}
|
||||
where
|
||||
-- queryString with '+' not converted to ' '
|
||||
queryStringWPlus = parseQueryReplacePlus False $ rawQueryString req
|
||||
-- queryString with '+' converted to ' '(space)
|
||||
qString = parseQueryReplacePlus True $ rawQueryString req
|
||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||
(filters, rpcQParams) =
|
||||
case action of
|
||||
@@ -215,7 +215,7 @@ userApiRequest schema rootSpec req reqBody
|
||||
path = pathInfo req
|
||||
method = requestMethod req
|
||||
hdrs = requestHeaders req
|
||||
qParams = [(toS k, v)|(k,v) <- queryStringWPlus]
|
||||
qParams = [(toS k, v)|(k,v) <- qString]
|
||||
lookupHeader = flip lookup hdrs
|
||||
hasPrefer :: Text -> Bool
|
||||
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
|
||||
|
||||
+6
-12
@@ -167,9 +167,9 @@ readOptions = do
|
||||
<*> pure False
|
||||
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings" C.value)
|
||||
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> optValue "role-claim-key")
|
||||
<*> (maybe ["public"] splitExtraSearchPath <$> optValue "db-extra-search-path")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
<*> ((\x y -> QualifiedIdentifier x <$> y) <$> dbSchema <*> optString "root-spec")
|
||||
<*> (fmap encodeUtf8 <$> optionalListOfText "raw-media-types")
|
||||
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
|
||||
|
||||
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
|
||||
parseJwtAudience k =
|
||||
@@ -180,12 +180,6 @@ readOptions = do
|
||||
(Just "") -> pure Nothing
|
||||
aud' -> pure aud'
|
||||
|
||||
optionalListOfText :: C.Key -> C.Parser C.Config [Text]
|
||||
optionalListOfText k =
|
||||
C.optional k (C.list C.string) >>= \case
|
||||
Nothing -> pure []
|
||||
Just types -> pure types
|
||||
|
||||
reqString :: C.Key -> C.Parser C.Config Text
|
||||
reqString k = C.required k C.string
|
||||
|
||||
@@ -219,9 +213,9 @@ readOptions = do
|
||||
parseRoleClaimKey (C.String s) = pRoleClaimKey s
|
||||
parseRoleClaimKey v = pRoleClaimKey $ show v
|
||||
|
||||
splitExtraSearchPath :: C.Value -> [Text]
|
||||
splitExtraSearchPath (C.String s) = strip <$> splitOn "," s
|
||||
splitExtraSearchPath _ = []
|
||||
splitOnCommas :: C.Value -> [Text]
|
||||
splitOnCommas (C.String s) = strip <$> splitOn "," s
|
||||
splitOnCommas _ = []
|
||||
|
||||
opts = info (helper <*> pathParser) $
|
||||
fullDesc
|
||||
@@ -283,7 +277,7 @@ readOptions = do
|
||||
|# root-spec = "stored_proc_name"
|
||||
|
|
||||
|## content types to produce raw output
|
||||
|# raw-media-types=["image/png","image/jpg"]
|
||||
|# raw-media-types="image/png, image/jpg"
|
||||
|]
|
||||
|
||||
pathParser :: Parser FilePath
|
||||
|
||||
@@ -437,7 +437,7 @@ allColumns tabs =
|
||||
nc.nspname::information_schema.sql_identifier AS table_schema,
|
||||
c.relname::information_schema.sql_identifier AS table_name,
|
||||
a.attname::information_schema.sql_identifier AS column_name,
|
||||
d.description::information_schema.sql_identifier AS description,
|
||||
d.description AS description,
|
||||
a.attnum::information_schema.cardinal_number AS ordinal_position,
|
||||
pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,
|
||||
CASE
|
||||
|
||||
@@ -224,7 +224,7 @@ instance JSON.ToJSON SimpleError where
|
||||
toJSON GucHeadersError = JSON.object [
|
||||
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
|
||||
toJSON (BinaryFieldError ct) = JSON.object [
|
||||
"message" .= ((toS (toMime ct) <> " requested but a single column was not selected") :: Text)]
|
||||
"message" .= ((toS (toMime ct) <> " requested but more than one column was selected") :: Text)]
|
||||
toJSON ConnectionLostError = JSON.object [
|
||||
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||
)
|
||||
)
|
||||
postOp = tOp
|
||||
& parameters .~ map ref ["body." <> tn, "preferReturn"]
|
||||
& parameters .~ map ref ["body." <> tn, "select", "preferReturn"]
|
||||
& at 201 ?~ "Created"
|
||||
patchOp = tOp
|
||||
& parameters .~ map ref (rs <> ["body." <> tn, "preferReturn"])
|
||||
|
||||
@@ -131,7 +131,7 @@ pRelationSelect = lexeme $ try ( do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
relationDetail <- optionMaybe (
|
||||
try ( char '+' *> pFieldName ) <|>
|
||||
try ( char '!' *> pFieldName ) <|>
|
||||
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version
|
||||
)
|
||||
|
||||
|
||||
+11
-10
@@ -480,12 +480,12 @@ spec actualPgVersion = do
|
||||
|
||||
describe "path fixed" $ do
|
||||
it "works when requesting children 2 levels" $
|
||||
get "/clients?id=eq.1&select=id,projects:projects%2Bclient_id(id,tasks(id))" `shouldRespondWith`
|
||||
get "/clients?id=eq.1&select=id,projects:projects!client_id(id,tasks(id))" `shouldRespondWith`
|
||||
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with parent relation" $
|
||||
get "/message?select=id,body,sender:person%2Bsender(name),recipient:person%2Brecipient(name)&id=lt.4" `shouldRespondWith`
|
||||
get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},
|
||||
{"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},
|
||||
@@ -499,7 +499,7 @@ spec actualPgVersion = do
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with a parent view relation" $
|
||||
get "/message?select=id,body,sender:person_detail%2Bsender(name,sent),recipient:person_detail%2Brecipient(name,received)&id=lt.4" `shouldRespondWith`
|
||||
get "/message?select=id,body,sender:person_detail!sender(name,sent),recipient:person_detail!recipient(name,received)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
|
||||
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
|
||||
@@ -507,10 +507,11 @@ spec actualPgVersion = do
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with many<->many relation" $
|
||||
get "/tasks?select=id,users:users%2Busers_tasks(id)" `shouldRespondWith`
|
||||
get "/tasks?select=id,users:users!users_tasks(id)" `shouldRespondWith`
|
||||
[json|[{"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":[]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
-- TODO Remove in next major version(7.0)
|
||||
describe "old dot '.' symbol, deprecated" $
|
||||
it "still works" $ do
|
||||
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
|
||||
@@ -588,7 +589,7 @@ spec actualPgVersion = do
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "embeds childs recursively" $
|
||||
get "/family_tree?id=eq.1&select=id,name, childs:family_tree%2Bparent(id,name,childs:family_tree%2Bparent(id,name))" `shouldRespondWith`
|
||||
get "/family_tree?id=eq.1&select=id,name, childs:family_tree!parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
|
||||
[json|[{
|
||||
"id": "1", "name": "Parental Unit", "childs": [
|
||||
{ "id": "2", "name": "Kid One", "childs": [ { "id": "4", "name": "Grandkid One" } ] },
|
||||
@@ -597,7 +598,7 @@ spec actualPgVersion = do
|
||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "embeds parent and then embeds childs" $
|
||||
get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree%2Bparent(id,name))" `shouldRespondWith`
|
||||
get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
|
||||
[json|[{
|
||||
"id": "2", "name": "Kid One", "parent": {
|
||||
"id": "1", "name": "Parental Unit", "childs": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]
|
||||
@@ -620,7 +621,7 @@ spec actualPgVersion = do
|
||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "embeds childs" $ do
|
||||
get "/organizations?select=id,name,refereeds:organizations%2Breferee(id,name)&id=eq.1" `shouldRespondWith`
|
||||
get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`
|
||||
[json|[{
|
||||
"id": 1, "name": "Referee Org",
|
||||
"refereeds": [
|
||||
@@ -634,7 +635,7 @@ spec actualPgVersion = do
|
||||
}
|
||||
]
|
||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
get "/organizations?select=id,name,auditees:organizations%2Bauditor(id,name)&id=eq.2" `shouldRespondWith`
|
||||
get "/organizations?select=id,name,auditees:organizations!auditor(id,name)&id=eq.2" `shouldRespondWith`
|
||||
[json|[{
|
||||
"id": 2, "name": "Auditor Org",
|
||||
"auditees": [
|
||||
@@ -668,7 +669,7 @@ spec actualPgVersion = do
|
||||
"manager":{"name":"Referee Manager"}}}
|
||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
get "/organizations?select=name,manager(name),auditees:organizations%2Bauditor(name,manager(name),refereeds:organizations%2Breferee(name,manager(name)))&id=eq.2" `shouldRespondWith`
|
||||
get "/organizations?select=name,manager(name),auditees:organizations!auditor(name,manager(name),refereeds:organizations!referee(name,manager(name)))&id=eq.2" `shouldRespondWith`
|
||||
[json|[{
|
||||
"name":"Auditor Org",
|
||||
"manager":{"name":"Auditor Manager"},
|
||||
@@ -950,7 +951,7 @@ spec actualPgVersion = do
|
||||
it "fails if a single column is not selected" $ do
|
||||
request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected"} |]
|
||||
{ matchStatus = 406
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ spec actualPgVersion =
|
||||
it "fails if a single column is not selected" $
|
||||
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
|
||||
`shouldRespondWith`
|
||||
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
|
||||
[json| {"message":"application/octet-stream requested but more than one column was selected"} |]
|
||||
{ matchStatus = 406
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ spec = do
|
||||
childGetSummary = r ^? method "get" . key "summary"
|
||||
childGetDescription = r ^? method "get" . key "description"
|
||||
getParameters = r ^? method "get" . key "parameters"
|
||||
postParameters = r ^? method "post" . key "parameters"
|
||||
postResponse = r ^? method "post" . key "responses" . key "201" . key "description"
|
||||
patchResponse = r ^? method "patch" . key "responses" . key "204" . key "description"
|
||||
deleteResponse = r ^? method "delete" . key "responses" . key "204" . key "description"
|
||||
@@ -79,6 +80,15 @@ spec = do
|
||||
]
|
||||
|]
|
||||
|
||||
postParameters `shouldBe` Just
|
||||
[aesonQQ|
|
||||
[
|
||||
{ "$ref": "#/parameters/body.child_entities" },
|
||||
{ "$ref": "#/parameters/select" },
|
||||
{ "$ref": "#/parameters/preferReturn" }
|
||||
]
|
||||
|]
|
||||
|
||||
postResponse `shouldBe` Just "Created"
|
||||
|
||||
patchResponse `shouldBe` Just "No Content"
|
||||
@@ -125,7 +135,7 @@ spec = do
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "child_entities name comment",
|
||||
"description": "child_entities name comment. Can be longer than sixty-three characters long",
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Vendored
+1
-1
@@ -1226,7 +1226,7 @@ create table ranges (
|
||||
|
||||
comment on table child_entities is 'child_entities comment';
|
||||
comment on column child_entities.id is 'child_entities id comment';
|
||||
comment on column child_entities.name is 'child_entities name comment';
|
||||
comment on column child_entities.name is 'child_entities name comment. Can be longer than sixty-three characters long';
|
||||
|
||||
comment on table grandchild_entities is
|
||||
$$grandchild_entities summary
|
||||
|
||||
Reference in New Issue
Block a user