Add 'PGRST' error code to differentiate from PostgreSQL errors
This commit is contained in:
+3
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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" <:>
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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" $
|
||||
|
||||
@@ -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] }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" $
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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" ]
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user