From d6834e8bf8752947fc7e868ce0fd3b8ab3b0c886 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 4 Mar 2022 15:57:38 -0500 Subject: [PATCH] Add 'PGRST' error code to differentiate from PostgreSQL errors --- CHANGELOG.md | 4 +- src/PostgREST/Error.hs | 277 ++++++++++++++---- test/spec/Feature/Auth/AuthSpec.hs | 4 +- test/spec/Feature/Auth/NoAnonSpec.hs | 6 +- test/spec/Feature/Auth/NoJwtSpec.hs | 6 +- test/spec/Feature/Query/AndOrParamsSpec.hs | 20 +- .../Feature/Query/EmbedDisambiguationSpec.hs | 24 +- test/spec/Feature/Query/ErrorSpec.hs | 18 +- test/spec/Feature/Query/InsertSpec.hs | 12 +- test/spec/Feature/Query/JsonOperatorSpec.hs | 12 +- test/spec/Feature/Query/MultipleSchemaSpec.hs | 6 +- test/spec/Feature/Query/QuerySpec.hs | 42 ++- test/spec/Feature/Query/RangeSpec.hs | 4 +- test/spec/Feature/Query/RpcSpec.hs | 60 ++-- test/spec/Feature/Query/SingularSpec.hs | 32 +- test/spec/Feature/Query/UpdateSpec.hs | 4 +- test/spec/Feature/Query/UpsertSpec.hs | 24 +- 17 files changed, 399 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aec9e17b..b9b2d217e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 44f2de6c8..60880c1ef 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -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" diff --git a/test/spec/Feature/Auth/AuthSpec.hs b/test/spec/Feature/Auth/AuthSpec.hs index a4238e576..fa0f17362 100644 --- a/test/spec/Feature/Auth/AuthSpec.hs +++ b/test/spec/Feature/Auth/AuthSpec.hs @@ -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" <:> diff --git a/test/spec/Feature/Auth/NoAnonSpec.hs b/test/spec/Feature/Auth/NoAnonSpec.hs index 836dd9e55..e3049fc32 100644 --- a/test/spec/Feature/Auth/NoAnonSpec.hs +++ b/test/spec/Feature/Auth/NoAnonSpec.hs @@ -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"] } diff --git a/test/spec/Feature/Auth/NoJwtSpec.hs b/test/spec/Feature/Auth/NoJwtSpec.hs index 82a179a7c..cccd1baf2 100644 --- a/test/spec/Feature/Auth/NoJwtSpec.hs +++ b/test/spec/Feature/Auth/NoJwtSpec.hs @@ -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" $ diff --git a/test/spec/Feature/Query/AndOrParamsSpec.hs b/test/spec/Feature/Query/AndOrParamsSpec.hs index b84d6031b..0048458df 100644 --- a/test/spec/Feature/Query/AndOrParamsSpec.hs +++ b/test/spec/Feature/Query/AndOrParamsSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/EmbedDisambiguationSpec.hs b/test/spec/Feature/Query/EmbedDisambiguationSpec.hs index e145ca6d9..ecf0a0783 100644 --- a/test/spec/Feature/Query/EmbedDisambiguationSpec.hs +++ b/test/spec/Feature/Query/EmbedDisambiguationSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/ErrorSpec.hs b/test/spec/Feature/Query/ErrorSpec.hs index ef843a429..561abdfa2 100644 --- a/test/spec/Feature/Query/ErrorSpec.hs +++ b/test/spec/Feature/Query/ErrorSpec.hs @@ -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 } diff --git a/test/spec/Feature/Query/InsertSpec.hs b/test/spec/Feature/Query/InsertSpec.hs index 3261af3ac..57622a223 100644 --- a/test/spec/Feature/Query/InsertSpec.hs +++ b/test/spec/Feature/Query/InsertSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/JsonOperatorSpec.hs b/test/spec/Feature/Query/JsonOperatorSpec.hs index 08596505d..af3f9cbfc 100644 --- a/test/spec/Feature/Query/JsonOperatorSpec.hs +++ b/test/spec/Feature/Query/JsonOperatorSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/MultipleSchemaSpec.hs b/test/spec/Feature/Query/MultipleSchemaSpec.hs index a7a7550c1..d1306b5cc 100644 --- a/test/spec/Feature/Query/MultipleSchemaSpec.hs +++ b/test/spec/Feature/Query/MultipleSchemaSpec.hs @@ -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 } diff --git a/test/spec/Feature/Query/QuerySpec.hs b/test/spec/Feature/Query/QuerySpec.hs index b673e9082..4f30b2bf8 100644 --- a/test/spec/Feature/Query/QuerySpec.hs +++ b/test/spec/Feature/Query/QuerySpec.hs @@ -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" $ diff --git a/test/spec/Feature/Query/RangeSpec.hs b/test/spec/Feature/Query/RangeSpec.hs index d765c80a3..64eee23e5 100644 --- a/test/spec/Feature/Query/RangeSpec.hs +++ b/test/spec/Feature/Query/RangeSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index e6ed4ef2e..fbc090668 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/SingularSpec.hs b/test/spec/Feature/Query/SingularSpec.hs index 62bcfdcbc..99df68c28 100644 --- a/test/spec/Feature/Query/SingularSpec.hs +++ b/test/spec/Feature/Query/SingularSpec.hs @@ -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" ] diff --git a/test/spec/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index 18602f30f..842cbda12 100644 --- a/test/spec/Feature/Query/UpdateSpec.hs +++ b/test/spec/Feature/Query/UpdateSpec.hs @@ -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] } diff --git a/test/spec/Feature/Query/UpsertSpec.hs b/test/spec/Feature/Query/UpsertSpec.hs index 604d82e45..67529d6e7 100644 --- a/test/spec/Feature/Query/UpsertSpec.hs +++ b/test/spec/Feature/Query/UpsertSpec.hs @@ -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