Add 'PGRST' error code to differentiate from PostgreSQL errors

This commit is contained in:
Laurence Isla
2022-03-04 15:57:38 -05:00
committed by GitHub
parent 99d0b805df
commit d6834e8bf8
17 changed files with 399 additions and 156 deletions
+3 -1
View File
@@ -37,7 +37,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2153, Fix --dump-schema running with a wrong PG version. - @wolfgangwalther
- #2042, Keep working when EMFILE(Too many open files) is reached. - @steve-chavez
- #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.
- ```
### Changed
@@ -61,6 +60,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)
- #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez
- #2031, Improve error message for ambiguous embedding and add a relevant hint that includes unambiguous embedding suggestions - @laurenceisla
- #1917, Add error codes with the `"PGRST"` prefix to the error response body to differentiate PostgREST errors from PostgreSQL errors - @laurenceisla
- #1917, Normalize the error response body by always having the `detail` and `hint` error fields with a `null` value if they are empty - @laurenceisla
- #2176, Errors raised with `SQLSTATE` now include the message and the code in the response body - @laurenceisla
### Fixed
+218 -59
View File
@@ -72,29 +72,65 @@ instance PgrstError ApiRequestError where
headers _ = [ContentType.toHeader CTApplicationJSON]
instance JSON.ToJSON ApiRequestError where
toJSON (ParseRequestError message details) = JSON.object [
"message" .= message, "details" .= details]
toJSON (QueryParamError (QPError message details)) = JSON.object [
"message" .= message, "details" .= details]
"code" .= ApiRequestErrorCode00,
"message" .= message,
"details" .= details,
"hint" .= JSON.Null]
toJSON ActionInappropriate = JSON.object [
"message" .= ("Bad Request" :: Text)]
"code" .= ApiRequestErrorCode01,
"message" .= ("Bad Request" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (InvalidBody errorMessage) = JSON.object [
"message" .= T.decodeUtf8 errorMessage]
"code" .= ApiRequestErrorCode02,
"message" .= T.decodeUtf8 errorMessage,
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON InvalidRange = JSON.object [
"message" .= ("HTTP Range error" :: Text)]
"code" .= ApiRequestErrorCode03,
"message" .= ("HTTP Range error" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (ParseRequestError message details) = JSON.object [
"code" .= ApiRequestErrorCode04,
"message" .= message,
"details" .= details,
"hint" .= JSON.Null]
toJSON InvalidFilters = JSON.object [
"code" .= ApiRequestErrorCode05,
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (UnacceptableSchema schemas) = JSON.object [
"code" .= ApiRequestErrorCode06,
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (ContentTypeError cts) = JSON.object [
"code" .= ApiRequestErrorCode07,
"message" .= ("None of these Content-Types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (NotEmbedded resource) = JSON.object [
"code" .= ApiRequestErrorCode08,
"message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text),
"details" .= JSON.Null,
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
toJSON (NoRelBetween parent child schema) = JSON.object [
"hint" .= ("Verify that '" <> parent <> "' and '" <> child <> "' exist in the schema '" <> schema <> "' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache." :: Text),
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text)]
"code" .= SchemaCacheErrorCode00,
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
"details" .= JSON.Null,
"hint" .= ("Verify that '" <> parent <> "' and '" <> child <> "' exist in the schema '" <> schema <> "' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache." :: Text)]
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text),
"code" .= SchemaCacheErrorCode01,
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
"details" .= (compressedRel <$> rels) ]
toJSON (AmbiguousRpc procs) = JSON.object [
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text),
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])]
"details" .= (compressedRel <$> rels),
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text),
"code" .= SchemaCacheErrorCode02,
"message" .= ("Could not find the " <> schema <> "." <> procName <>
(case (hasPreferSingleObject, isInvPost, contentType) of
(True, _, _) -> " function with a single json or jsonb parameter"
@@ -102,16 +138,14 @@ instance JSON.ToJSON ApiRequestError where
(_, True, CTOctetStream) -> " function with a single unnamed bytea parameter"
(_, True, CTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
_ -> prms <> " function") <>
" in the schema cache")]
toJSON InvalidFilters = JSON.object [
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
toJSON (UnacceptableSchema schemas) = JSON.object [
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
toJSON (ContentTypeError cts) = JSON.object [
"message" .= ("None of these Content-Types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts))]
toJSON (NotEmbedded resource) = JSON.object [
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text),
"message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text)]
" in the schema cache"),
"details" .= JSON.Null,
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text)]
toJSON (AmbiguousRpc procs) = JSON.object [
"code" .= SchemaCacheErrorCode03,
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
"details" .= JSON.Null,
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
compressedRel :: Relationship -> JSON.Value
compressedRel Relationship{..} =
@@ -161,46 +195,52 @@ instance JSON.ToJSON PgError where
instance JSON.ToJSON SQL.UsageError where
toJSON (SQL.ConnectionError e) = JSON.object [
"code" .= ("" :: Text),
"code" .= ConnectionErrorCode00,
"message" .= ("Database connection error. Retrying the connection." :: Text),
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text)]
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text),
"hint" .= JSON.Null]
toJSON (SQL.SessionError e) = JSON.toJSON e -- SQL.Error
instance JSON.ToJSON SQL.QueryError where
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
instance JSON.ToJSON SQL.CommandError where
toJSON (SQL.ResultError (SQL.ServerError c m d h)) = case BS.unpack c of
'P':'T':_ -> JSON.object [
"details" .= fmap T.decodeUtf8 d,
"hint" .= fmap T.decodeUtf8 h]
_ -> JSON.object [
toJSON (SQL.ResultError (SQL.ServerError c m d h)) = JSON.object [
"code" .= (T.decodeUtf8 c :: Text),
"message" .= (T.decodeUtf8 m :: Text),
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
toJSON (SQL.ResultError (SQL.UnexpectedResult m)) = JSON.object [
"message" .= (m :: Text)]
"code" .= HasqlErrorCode00,
"message" .= (m :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (SQL.ResultError (SQL.RowError i SQL.EndOfInput)) = JSON.object [
"code" .= HasqlErrorCode01,
"message" .= ("Row error: end of input" :: Text),
"details" .= ("Attempt to parse more columns than there are in the result" :: Text),
"hint" .= (("Row number " <> show i) :: Text)]
toJSON (SQL.ResultError (SQL.RowError i SQL.UnexpectedNull)) = JSON.object [
"code" .= HasqlErrorCode02,
"message" .= ("Row error: unexpected null" :: Text),
"details" .= ("Attempt to parse a NULL as some value." :: Text),
"hint" .= (("Row number " <> show i) :: Text)]
toJSON (SQL.ResultError (SQL.RowError i (SQL.ValueError d))) = JSON.object [
"code" .= HasqlErrorCode03,
"message" .= ("Row error: Wrong value parser used" :: Text),
"details" .= d,
"hint" .= (("Row number " <> show i) :: Text)]
toJSON (SQL.ResultError (SQL.UnexpectedAmountOfRows i)) = JSON.object [
"code" .= HasqlErrorCode04,
"message" .= ("Unexpected amount of rows" :: Text),
"details" .= i]
"details" .= i,
"hint" .= JSON.Null]
toJSON (SQL.ClientError d) = JSON.object [
"code" .= ConnectionErrorCode01,
"message" .= ("Database client error. Retrying the connection." :: Text),
"details" .= (fmap T.decodeUtf8 d :: Maybe Text)]
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
"hint" .= JSON.Null]
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
pgErrorStatus _ (SQL.ConnectionError _) = HTTP.status503
@@ -306,33 +346,68 @@ instance PgrstError Error where
headers _ = [ContentType.toHeader CTApplicationJSON]
instance JSON.ToJSON Error 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 GucStatusError = JSON.object [
"message" .= ("response.status guc must be a valid status code" :: Text)]
toJSON (BinaryFieldError ct) = JSON.object [
"message" .= ((T.decodeUtf8 (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text)]
toJSON NoSchemaCacheError = JSON.object [
"message" .= ("Could not query the database for the schema cache. Retrying." :: Text)]
toJSON NoSchemaCacheError = JSON.object [
"code" .= ConnectionErrorCode02,
"message" .= ("Could not query the database for the schema cache. Retrying." :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON PutRangeNotAllowedError = JSON.object [
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]
toJSON PutMatchingPkError = JSON.object [
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
toJSON (SingularityError n) = JSON.object [
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
"details" .= T.unwords ["Results contain", show n, "rows,", T.decodeUtf8 (ContentType.toMime CTSingularJSON), "requires 1 row"]]
toJSON JwtTokenMissing = JSON.object [
"message" .= ("Server lacks JWT secret" :: Text)]
toJSON JwtTokenMissing = JSON.object [
"code" .= JWTErrorCode00,
"message" .= ("Server lacks JWT secret" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (JwtTokenInvalid message) = JSON.object [
"message" .= (message :: Text)]
toJSON JwtTokenRequired = JSON.object [
"message" .= ("Anonymous access is disabled" :: Text)]
toJSON NotFound = JSON.object []
"code" .= JWTErrorCode01,
"message" .= (message :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON JwtTokenRequired = JSON.object [
"code" .= JWTErrorCode02,
"message" .= ("Anonymous access is disabled" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON GucHeadersError = JSON.object [
"code" .= GeneralErrorCode00,
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON GucStatusError = JSON.object [
"code" .= GeneralErrorCode01,
"message" .= ("response.status guc must be a valid status code" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (BinaryFieldError ct) = JSON.object [
"code" .= GeneralErrorCode02,
"message" .= ((T.decodeUtf8 (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON PutRangeNotAllowedError = JSON.object [
"code" .= GeneralErrorCode03,
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON PutMatchingPkError = JSON.object [
"code" .= GeneralErrorCode04,
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (SingularityError n) = JSON.object [
"code" .= GeneralErrorCode05,
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
"details" .= T.unwords ["Results contain", show n, "rows,", T.decodeUtf8 (ContentType.toMime CTSingularJSON), "requires 1 row"],
"hint" .= JSON.Null]
toJSON (UnsupportedVerb verb) = JSON.object [
"message" .= ("Unsupported HTTP verb: " <> verb)]
"code" .= GeneralErrorCode06,
"message" .= ("Unsupported HTTP verb: " <> verb),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON NotFound = JSON.object []
toJSON (PgErr err) = JSON.toJSON err
toJSON (ApiRequestError err) = JSON.toJSON err
@@ -345,3 +420,87 @@ requiredTokenHeader = ("WWW-Authenticate", "Bearer")
singularityError :: (Integral a) => a -> Error
singularityError = SingularityError . toInteger
-- Error codes are grouped by common modules or characteristics
data ErrorCode
-- PostgreSQL connection errors
= ConnectionErrorCode00
| ConnectionErrorCode01
| ConnectionErrorCode02
-- API Request errors
| ApiRequestErrorCode00
| ApiRequestErrorCode01
| ApiRequestErrorCode02
| ApiRequestErrorCode03
| ApiRequestErrorCode04
| ApiRequestErrorCode05
| ApiRequestErrorCode06
| ApiRequestErrorCode07
| ApiRequestErrorCode08
-- Schema Cache errors
| SchemaCacheErrorCode00
| SchemaCacheErrorCode01
| SchemaCacheErrorCode02
| SchemaCacheErrorCode03
-- JWT authentication errors
| JWTErrorCode00
| JWTErrorCode01
| JWTErrorCode02
-- Hasql library errors
| HasqlErrorCode00
| HasqlErrorCode01
| HasqlErrorCode02
| HasqlErrorCode03
| HasqlErrorCode04
-- Uncategorized errors that are not related to a single module
| GeneralErrorCode00
| GeneralErrorCode01
| GeneralErrorCode02
| GeneralErrorCode03
| GeneralErrorCode04
| GeneralErrorCode05
| GeneralErrorCode06
instance JSON.ToJSON ErrorCode where
toJSON e = JSON.toJSON (buildErrorCode e)
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
-- New errors are added at the end of the group they belong to and will have the next code in the sequence
buildErrorCode :: ErrorCode -> Text
buildErrorCode code = "PGRST" <> case code of
ConnectionErrorCode00 -> "000"
ConnectionErrorCode01 -> "001"
ConnectionErrorCode02 -> "002"
ApiRequestErrorCode00 -> "100"
ApiRequestErrorCode01 -> "101"
ApiRequestErrorCode02 -> "102"
ApiRequestErrorCode03 -> "103"
ApiRequestErrorCode04 -> "104"
ApiRequestErrorCode05 -> "105"
ApiRequestErrorCode06 -> "106"
ApiRequestErrorCode07 -> "107"
ApiRequestErrorCode08 -> "108"
SchemaCacheErrorCode00 -> "200"
SchemaCacheErrorCode01 -> "201"
SchemaCacheErrorCode02 -> "202"
SchemaCacheErrorCode03 -> "203"
JWTErrorCode00 -> "300"
JWTErrorCode01 -> "301"
JWTErrorCode02 -> "302"
HasqlErrorCode00 -> "400"
HasqlErrorCode01 -> "401"
HasqlErrorCode02 -> "402"
HasqlErrorCode03 -> "403"
HasqlErrorCode04 -> "404"
GeneralErrorCode00 -> "500"
GeneralErrorCode01 -> "501"
GeneralErrorCode02 -> "502"
GeneralErrorCode03 -> "503"
GeneralErrorCode04 -> "504"
GeneralErrorCode05 -> "505"
GeneralErrorCode06 -> "506"
+2 -2
View File
@@ -106,7 +106,7 @@ spec actualPgVersion = describe "authorization" $ do
it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWT expired"} |]
`shouldRespondWith` [json| {"message":"JWT expired","code":"PGRST301","hint":null,"details":null} |]
{ matchStatus = 401
, matchHeaders = [
"WWW-Authenticate" <:>
@@ -117,7 +117,7 @@ spec actualPgVersion = describe "authorization" $ do
it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)"} |]
`shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 2)","code":"PGRST301","hint":null,"details":null} |]
{ matchStatus = 401
, matchHeaders = [
"WWW-Authenticate" <:>
+5 -1
View File
@@ -24,7 +24,11 @@ spec = describe "server started without anonymous role" $ do
it "responds with error when user does not attempt auth" $
get "/items"
`shouldRespondWith`
[json|{"message":"Anonymous access is disabled"}|]
[json|
{"hint": null,
"details": null,
"code": "PGRST302",
"message":"Anonymous access is disabled"}|]
{ matchStatus = 401
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
}
+5 -1
View File
@@ -20,7 +20,11 @@ spec = describe "server started without JWT secret" $ do
[auth]
""
`shouldRespondWith`
[json|{"message":"Server lacks JWT secret"}|]
[json|
{"hint": null,
"details": null,
"code": "PGRST300",
"message": "Server lacks JWT secret"}|]
{ matchStatus = 500 }
it "behaves normally when user does not attempt auth" $
+15 -5
View File
@@ -201,7 +201,9 @@ spec actualPgVersion =
get "/entities?or=()" `shouldRespondWith`
[json|{
"details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
"message": "\"failed to parse logic tree (())\" (line 1, column 4)"
"message": "\"failed to parse logic tree (())\" (line 1, column 4)",
"code": "PGRST100",
"hint": null
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
it "can have a single condition" $ do
get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`
@@ -255,22 +257,30 @@ spec actualPgVersion =
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
[json|{
"details": "unexpected \"1\" expecting \"(\"",
"message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
"message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)",
"code": "PGRST100",
"hint": null
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
it "fails on malformed query params and provides meaningful error message" $ do
get "/entities?or=)(" `shouldRespondWith`
[json|{
"details": "unexpected \")\" expecting \"(\"",
"message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
"message": "\"failed to parse logic tree ()()\" (line 1, column 3)",
"code": "PGRST100",
"hint": null
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
[json|{
"details": "unexpected \"d\" expecting \"(\"",
"message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
"message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)",
"code": "PGRST100",
"hint": null
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
[json|{
"details": "unexpected \"x\" expecting logic operator (and, or)",
"message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
"message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)",
"code": "PGRST100",
"hint": null
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
@@ -30,7 +30,8 @@ spec =
}
],
"hint": "Try changing 'sender' to one of the following: 'person!message_sender_fkey', 'person_detail!message_sender_fkey'. Find the desired relationship in the 'details' key.",
"message": "Could not embed because more than one relationship was found for 'message' and 'sender'"
"message": "Could not embed because more than one relationship was found for 'message' and 'sender'",
"code": "PGRST201"
}
|]
{ matchStatus = 300
@@ -59,7 +60,8 @@ spec =
}
],
"hint": "Try changing 'big_projects' to one of the following: 'big_projects!main_project', 'big_projects!jobs', 'big_projects!main_jobs'. Find the desired relationship in the 'details' key.",
"message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'"
"message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'",
"code": "PGRST201"
}
|]
{ matchStatus = 300
@@ -83,7 +85,8 @@ spec =
}
],
"hint": "Try changing 'departments' to one of the following: 'departments!agents_department_id_fkey', 'departments!departments_head_id_fkey'. Find the desired relationship in the 'details' key.",
"message": "Could not embed because more than one relationship was found for 'agents' and 'departments'"
"message": "Could not embed because more than one relationship was found for 'agents' and 'departments'",
"code": "PGRST201"
}
|]
{ matchStatus = 300
@@ -120,7 +123,8 @@ spec =
}
],
"hint": "Try changing 'whatev_projects' to one of the following: 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs'. Find the desired relationship in the 'details' key.",
"message": "Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'"
"message": "Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'",
"code": "PGRST201"
}
|]
{ matchStatus = 300
@@ -175,7 +179,9 @@ spec =
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
[json|{
"hint":"Verify that 'message' and 'person' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"message":"Could not find a relationship between 'message' and 'person' in the schema cache"}|]
"message":"Could not find a relationship between 'message' and 'person' in the schema cache",
"code": "PGRST200",
"details": null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
@@ -426,7 +432,9 @@ spec =
get "/end_1?select=end_2(*)" `shouldRespondWith`
[json|{
"hint":"Verify that 'end_1' and 'end_2' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"message":"Could not find a relationship between 'end_1' and 'end_2' in the schema cache"}|]
"message":"Could not find a relationship between 'end_1' and 'end_2' in the schema cache",
"code":"PGRST200",
"details": null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
it "shouldn't try to embed if the private junction has an exposed homonym" $
@@ -435,6 +443,8 @@ spec =
get "/schauspieler?select=filme(*)" `shouldRespondWith`
[json|{
"hint":"Verify that 'schauspieler' and 'filme' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"message":"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache"}|]
"message":"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache",
"code":"PGRST200",
"details": null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
+15 -3
View File
@@ -24,7 +24,11 @@ spec = do
[]
""
`shouldRespondWith`
[json|{"message":"Unsupported HTTP verb: CONNECT"}|]
[json|
{"hint": null,
"details": null,
"code": "PGRST506",
"message":"Unsupported HTTP verb: CONNECT"}|]
{ matchStatus = 405 }
it "should return 405 for TRACE method" $
@@ -32,7 +36,11 @@ spec = do
[]
""
`shouldRespondWith`
[json|{"message":"Unsupported HTTP verb: TRACE"}|]
[json|
{"hint": null,
"details": null,
"code": "PGRST506",
"message":"Unsupported HTTP verb: TRACE"}|]
{ matchStatus = 405 }
it "should return 405 for OTHER method" $
@@ -40,5 +48,9 @@ spec = do
[]
""
`shouldRespondWith`
[json|{"message":"Unsupported HTTP verb: OTHER"}|]
[json|
{"hint": null,
"details": null,
"code": "PGRST506",
"message":"Unsupported HTTP verb: OTHER"}|]
{ matchStatus = 405 }
+6 -6
View File
@@ -75,7 +75,7 @@ spec actualPgVersion = do
post "/articles"
[json| [{"id": 100, "body": "xxxxx"}, 123, "xxxx", {"id": 111, "body": "xxxx"}] |]
`shouldRespondWith`
[json| {"message":"All object keys must match"} |]
[json| {"message":"All object keys must match","code":"PGRST102","hint":null,"details":null} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -84,7 +84,7 @@ spec actualPgVersion = do
post "/articles"
[json| [{"id": 100, "body": "xxxxx"}, {"id": 111, "body": "xxxx", "owner": "me"}] |]
`shouldRespondWith`
[json| {"message":"All object keys must match"} |]
[json| {"message":"All object keys must match","code":"PGRST102","hint":null,"details":null} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -276,7 +276,7 @@ spec actualPgVersion = do
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2"
`shouldRespondWith`
[json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]
[json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -285,7 +285,7 @@ spec actualPgVersion = do
it "fails with 400 and error" $
post "/simple_pk" ""
`shouldRespondWith`
[json|{"message":"Error in $: not enough input"}|]
[json|{"message":"Error in $: not enough input","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -415,7 +415,7 @@ spec actualPgVersion = do
{"id": 204, "body": "yyy"},
{"id": 205, "body": "zzz"}]|]
`shouldRespondWith`
[json| {"details":"unexpected end of input expecting field name (* or [a..z0..9_])","message":"\"failed to parse columns parameter ()\" (line 1, column 1)"} |]
[json| {"details":"unexpected end of input expecting field name (* or [a..z0..9_])","message":"\"failed to parse columns parameter ()\" (line 1, column 1)","code":"PGRST100","hint":null} |]
{ matchStatus = 400
, matchHeaders = []
}
@@ -484,7 +484,7 @@ spec actualPgVersion = do
it "fails for too few" $
request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
`shouldRespondWith`
[json|{"message":"All lines must have same number of fields"}|]
[json|{"message":"All lines must have same number of fields","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
+9 -3
View File
@@ -296,15 +296,21 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
[json|
{"details": "unexpected 'x' expecting digit, \"->\", \"::\", \".\", \",\" or end of input",
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)",
"code": "PGRST100",
"hint": null} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>--34" `shouldRespondWith`
[json|
{"details": "unexpected \"-\" expecting digit",
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)",
"code": "PGRST100",
"hint": null} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
[json|
{"details":"unexpected \"x\" expecting digit",
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)",
"code": "PGRST100",
"hint": null} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
@@ -68,7 +68,7 @@ spec =
it "fails trying to read table from unkown schema" $
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
[json|{"message":"The schema must be one of the following: v1, v2","code":"PGRST106","details":null,"hint":null}|]
{
matchStatus = 406
}
@@ -111,7 +111,7 @@ spec =
request methodPost "/children" [("Content-Profile", "unknown")]
[json|{"name": "child 4", "parent_id": 4}|]
`shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
[json|{"message":"The schema must be one of the following: v1, v2","code":"PGRST106","details":null,"hint":null}|]
{
matchStatus = 406
}
@@ -325,7 +325,7 @@ spec =
it "fails trying to read definitions from unkown schema" $
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
[json|{"message":"The schema must be one of the following: v1, v2","code":"PGRST106","details":null,"hint":null}|]
{
matchStatus = 406
}
+28 -14
View File
@@ -266,13 +266,17 @@ spec actualPgVersion = do
get "/clients?select=*&non_existent_projects.name=like.*NonExistent*" `shouldRespondWith`
[json|
{"hint":"Verify that 'non_existent_projects' is included in the 'select' query parameter.",
"message":"Cannot apply filter because 'non_existent_projects' is not an embedded resource in this request"}|]
"details":null,
"code":"PGRST108",
"message":"Cannot apply filter because 'non_existent_projects' is not an embedded resource in this request"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith`
[json|
{"hint":"Verify that 'amiga_projectsss' is included in the 'select' query parameter.",
"details":null,
"code":"PGRST108",
"message":"Cannot apply filter because 'amiga_projectsss' is not an embedded resource in this request"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -280,6 +284,8 @@ spec actualPgVersion = do
get "/clients?select=id,projects(id,tasks(id,name))&projects.tasks2.name=like.Design*" `shouldRespondWith`
[json|
{"hint":"Verify that 'tasks2' is included in the 'select' query parameter.",
"details":null,
"code":"PGRST108",
"message":"Cannot apply filter because 'tasks2' is not an embedded resource in this request"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -504,6 +510,8 @@ spec actualPgVersion = do
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
[json|
{"hint":"Verify that 'car_models' and 'car_model_sales_202101' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"details":null,
"code":"PGRST200",
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -513,6 +521,8 @@ spec actualPgVersion = do
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
[json|
{"hint":"Verify that 'car_model_sales_202101' and 'car_models' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"details":null,
"code":"PGRST200",
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -522,6 +532,8 @@ spec actualPgVersion = do
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
[json|
{"hint":"Verify that 'car_model_sales' and 'car_models_default' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"details":null,
"code":"PGRST200",
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -531,6 +543,8 @@ spec actualPgVersion = do
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
[json|
{"hint":"Verify that 'car_models_default' and 'car_model_sales' exist in the schema 'test' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.",
"details":null,
"code":"PGRST200",
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
@@ -812,44 +826,44 @@ spec actualPgVersion = do
context "order syntax errors" $ do
it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do
get "/items?order=id.ac" `shouldRespondWith`
[json|{"details":"unexpected \"c\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.ac)\" (line 1, column 4)"}|]
[json|{"details":"unexpected \"c\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.ac)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.descc" `shouldRespondWith`
[json|{"details":"unexpected 'c' expecting delimiter (.), \",\" or end of input","message":"\"failed to parse order (id.descc)\" (line 1, column 8)"}|]
[json|{"details":"unexpected 'c' expecting delimiter (.), \",\" or end of input","message":"\"failed to parse order (id.descc)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.nulsfist" `shouldRespondWith`
[json|{"details":"unexpected \"n\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.nulsfist)\" (line 1, column 4)"}|]
[json|{"details":"unexpected \"n\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.nulsfist)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.nullslasttt" `shouldRespondWith`
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.nullslasttt)\" (line 1, column 13)"}|]
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.nullslasttt)\" (line 1, column 13)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.smth34" `shouldRespondWith`
[json|{"details":"unexpected \"s\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.smth34)\" (line 1, column 4)"}|]
[json|{"details":"unexpected \"s\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.smth34)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "gives meaningful error messages when nulls{first,last} are misspelled after asc/desc" $ do
get "/items?order=id.asc.nlsfst" `shouldRespondWith`
[json|{"details":"unexpected \"l\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.nlsfst)\" (line 1, column 8)"}|]
[json|{"details":"unexpected \"l\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.nlsfst)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)"}|]
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/items?order=id.asc.smth34" `shouldRespondWith`
[json|{"details":"unexpected \"s\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.smth34)\" (line 1, column 8)"}|]
[json|{"details":"unexpected \"s\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.smth34)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -859,7 +873,7 @@ spec actualPgVersion = do
request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype") ""
`shouldRespondWith`
[json|{"message":"None of these Content-Types are available: text/unknowntype"}|]
[json|{"message":"None of these Content-Types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
{ matchStatus = 415
, matchHeaders = [matchContentTypeJson]
}
@@ -928,7 +942,7 @@ spec actualPgVersion = do
{ matchHeaders = [matchContentTypeJson] }
it "fails if an operator is not given" $
get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"Failed to parse [(\"id\",\"0\")]","message":"Unexpected param or filter missing operator"} |]
get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"Failed to parse [(\"id\",\"0\")]","message":"Unexpected param or filter missing operator","code":"PGRST104","hint":null} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
@@ -970,21 +984,21 @@ 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 more than one column was selected"} |]
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST502","details":null,"hint":null} |]
{ matchStatus = 406 }
request methodGet "/images?select=*&name=eq.A.png"
(acceptHdrs "application/octet-stream")
""
`shouldRespondWith`
[json| {"message":"application/octet-stream requested but more than one column was selected"} |]
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST502","details":null,"hint":null} |]
{ matchStatus = 406 }
request methodGet "/images?name=eq.A.png"
(acceptHdrs "application/octet-stream")
""
`shouldRespondWith`
[json| {"message":"application/octet-stream requested but more than one column was selected"} |]
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST502","details":null,"hint":null} |]
{ matchStatus = 406 }
it "concatenates results if more than one row is returned" $
+2 -2
View File
@@ -186,14 +186,14 @@ spec = do
it "fails if limit equals 0" $
get "/items?select=id&limit=0"
`shouldRespondWith` [json|{"message":"HTTP Range error"}|]
`shouldRespondWith` [json|{"message":"HTTP Range error","code":"PGRST103","details":null,"hint":null}|]
{ matchStatus = 416
, matchHeaders = [matchContentTypeJson]
}
it "fails if limit is negative" $
get "/items?select=id&limit=-1"
`shouldRespondWith` [json|{"message":"HTTP Range error"}|]
`shouldRespondWith` [json|{"message":"HTTP Range error","code":"PGRST103","details":null,"hint":null}|]
{ matchStatus = 416
, matchHeaders = [matchContentTypeJson]
}
+41 -19
View File
@@ -114,7 +114,9 @@ spec actualPgVersion =
get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`
[json| {
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache" } |]
"message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache",
"code":"PGRST202",
"details":null} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
@@ -126,7 +128,9 @@ spec actualPgVersion =
`shouldRespondWith`
[json| {
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache" } |]
"message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache",
"code":"PGRST202",
"details":null} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
@@ -135,14 +139,18 @@ spec actualPgVersion =
get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`
[json| {
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message":"Could not find the test.overloaded(wrong_arg) function in the schema cache" } |]
"message":"Could not find the test.overloaded(wrong_arg) function in the schema cache",
"code":"PGRST202",
"details":null} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`
[json| {
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache" } |]
"message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache",
"code":"PGRST202",
"details":null} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
@@ -152,7 +160,9 @@ spec actualPgVersion =
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
[json| {
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)",
"code":"PGRST203",
"details":null} |]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
@@ -510,7 +520,7 @@ spec actualPgVersion =
it "DELETE fails" $
request methodDelete "/rpc/sayhello" [] ""
`shouldRespondWith`
[json|{"message":"Bad Request"}|]
[json|{"message":"Bad Request","code":"PGRST101","details":null,"hint":null}|]
{ matchStatus = 405
, matchHeaders = [matchContentTypeJson]
}
@@ -671,7 +681,7 @@ spec actualPgVersion =
it "can map a RAISE error code and message to a http status" $
get "/rpc/raise_pt402"
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded" }|]
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|]
{ matchStatus = 402
, matchHeaders = [matchContentTypeJson]
}
@@ -679,7 +689,7 @@ spec actualPgVersion =
it "defaults to status 500 if RAISE code is PT not followed by a number" $
get "/rpc/raise_bad_pt"
`shouldRespondWith`
[json|{"hint": null, "details": null}|]
[json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
@@ -983,7 +993,7 @@ spec actualPgVersion =
request methodPost "/rpc/ret_rows_with_base64_bin"
(acceptHdrs "application/octet-stream") ""
`shouldRespondWith`
[json| {"message":"application/octet-stream requested but more than one column was selected"} |]
[json| {"message":"application/octet-stream requested but more than one column was selected","code":"PGRST502","details":null,"hint":null} |]
{ matchStatus = 406 }
context "only for GET rpc" $ do
@@ -1053,25 +1063,25 @@ spec actualPgVersion =
it "fails when setting headers with wrong json structure" $ do
get "/rpc/bad_guc_headers_1"
`shouldRespondWith`
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST500","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
get "/rpc/bad_guc_headers_2"
`shouldRespondWith`
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST500","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
get "/rpc/bad_guc_headers_3"
`shouldRespondWith`
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST500","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
post "/rpc/bad_guc_headers_1" [json|{}|]
`shouldRespondWith`
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST500","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
@@ -1142,7 +1152,7 @@ spec actualPgVersion =
it "fails when setting invalid status guc" $
get "/rpc/send_bad_status"
`shouldRespondWith`
[json|{"message":"response.status guc must be a valid status code"}|]
[json|{"message":"response.status guc must be a valid status code","code":"PGRST501","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
@@ -1176,7 +1186,9 @@ spec actualPgVersion =
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache",
"code":"PGRST202",
"details":null
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
@@ -1189,7 +1201,9 @@ spec actualPgVersion =
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache",
"code":"PGRST202",
"details":null
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
@@ -1203,7 +1217,9 @@ spec actualPgVersion =
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache",
"code":"PGRST202",
"details":null
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
@@ -1261,7 +1277,10 @@ spec actualPgVersion =
`shouldRespondWith`
[json| {
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache"}|]
"message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache",
"code":"PGRST202",
"details":null
}|]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
}
@@ -1272,7 +1291,10 @@ spec actualPgVersion =
`shouldRespondWith`
[json| {
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)"}|]
"message":"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)",
"code":"PGRST203",
"details":null
}|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
+16 -16
View File
@@ -69,7 +69,7 @@ spec =
[("Prefer", "tx=commit"), singular]
[json| { address: "zzz" } |]
`shouldRespondWith`
[json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -85,7 +85,7 @@ spec =
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
[json| { address: "zzz" } |]
`shouldRespondWith`
[json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -100,7 +100,7 @@ spec =
request methodPatch "/items?id=gt.0&id=lt.0"
[singular] [json|{"id":1}|]
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -109,7 +109,7 @@ spec =
request methodPatch "/items?id=gt.0&id=lt.0"
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -141,7 +141,7 @@ spec =
[("Prefer", "tx=commit"), singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
`shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -157,7 +157,7 @@ spec =
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
[json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]
`shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -173,7 +173,7 @@ spec =
[("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular]
[json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]
`shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -189,7 +189,7 @@ spec =
[singular]
[json| [ ] |]
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -199,7 +199,7 @@ spec =
[("Prefer", "return=representation"), singular]
[json| [ ] |]
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -222,7 +222,7 @@ spec =
[("Prefer", "tx=commit"), singular]
""
`shouldRespondWith`
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -240,7 +240,7 @@ spec =
request methodDelete "/items?id=gt.5&id=lt.11"
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] ""
`shouldRespondWith`
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
@@ -257,7 +257,7 @@ spec =
request methodDelete "/items?id=lt.0"
[singular] ""
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -266,7 +266,7 @@ spec =
request methodDelete "/items?id=lt.0"
[("Prefer", "return=representation"), singular] ""
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -276,7 +276,7 @@ spec =
request methodPost "/rpc/getproject"
[singular] [json|{ "id": 9999999}|]
`shouldRespondWith`
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -299,7 +299,7 @@ spec =
request methodPost "/rpc/getallprojects"
[singular] "{}"
`shouldRespondWith`
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
@@ -314,7 +314,7 @@ spec =
[("Prefer", "tx=commit"), singular]
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
`shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned","code":"PGRST505","hint":null}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
+2 -2
View File
@@ -33,7 +33,7 @@ spec = do
it "fails with 400 and error" $
request methodPatch "/simple_pk" [] "}{ x = 2"
`shouldRespondWith`
[json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]
[json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson]
}
@@ -42,7 +42,7 @@ spec = do
it "fails with 400 and error" $
request methodPatch "/items" [] ""
`shouldRespondWith`
[json|{"message":"Error in $: not enough input"}|]
[json|{"message":"Error in $: not enough input","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson]
}
+12 -12
View File
@@ -199,73 +199,73 @@ spec actualPgVersion =
request methodPut "/tiobe_pls?name=eq.Javascript" [("Range", "0-5")]
[json| [ { "name": "Javascript", "rank": 1 } ]|]
`shouldRespondWith`
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST503","details":null,"hint":null}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "fails if limit is specified" $
put "/tiobe_pls?name=eq.Javascript&limit=1"
[json| [ { "name": "Javascript", "rank": 1 } ]|]
`shouldRespondWith`
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST503","details":null,"hint":null}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "fails if offset is specified" $
put "/tiobe_pls?name=eq.Javascript&offset=1"
[json| [ { "name": "Javascript", "rank": 1 } ]|]
`shouldRespondWith`
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST503","details":null,"hint":null}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "rejects every other filter than pk cols eq's" $ do
put "/tiobe_pls?rank=eq.19"
[json| [ { "name": "Go", "rank": 19 } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
put "/tiobe_pls?id=not.eq.Java"
[json| [ { "name": "Go", "rank": 19 } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
put "/tiobe_pls?id=in.(Go)"
[json| [ { "name": "Go", "rank": 19 } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
put "/tiobe_pls?and=(id.eq.Go)"
[json| [ { "name": "Go", "rank": 19 } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
it "fails if not all composite key cols are specified as eq filters" $ do
put "/employees?first_name=eq.Susan"
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
put "/employees?last_name=eq.Heidt"
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
it "fails if the uri primary key doesn't match the payload primary key" $ do
put "/tiobe_pls?name=eq.MATLAB" [json| [ { "name": "Perl", "rank": 17 } ]|]
`shouldRespondWith`
[json|{"message":"Payload values do not match URL in primary key column(s)"}|]
[json|{"message":"Payload values do not match URL in primary key column(s)","code":"PGRST504","details":null,"hint":null}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
put "/employees?first_name=eq.Wendy&last_name=eq.Anderson"
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith`
[json|{"message":"Payload values do not match URL in primary key column(s)"}|]
[json|{"message":"Payload values do not match URL in primary key column(s)","code":"PGRST504","details":null,"hint":null}|]
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
it "fails if the table has no PK" $
put "/no_pk?a=eq.one&b=eq.two" [json| [ { "a": "one", "b": "two" } ]|]
`shouldRespondWith`
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators"}|]
[json|{"message":"Filters must include all and only primary key columns with 'eq' operators","code":"PGRST105","details":null,"hint":null}|]
{ matchStatus = 405 , matchHeaders = [matchContentTypeJson] }
context "Inserting row" $ do