Add suggestions with fuzzy text search when no relationship is found (#2583)
This commit is contained in:
committed by
Steve Chavez
parent
171dd313d9
commit
9d4ff812c9
+2
-1
@@ -8,7 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- #2565, Fix bad M2M embedding on RPC - @steve-chavez
|
- #2565, Fix bad M2M embedding on RPC - @steve-chavez
|
||||||
|
- #2575, Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions - @laurenceisla
|
||||||
|
- #2569, Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions - @laurenceisla
|
||||||
## [10.1.1] - 2022-11-08
|
## [10.1.1] - 2022-11-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import PostgREST.MediaType (MediaType (..))
|
|||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Relationship)
|
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||||
|
RelationshipsMap)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ data ApiRequestError
|
|||||||
| InvalidRpcMethod ByteString
|
| InvalidRpcMethod ByteString
|
||||||
| LimitNoOrderError
|
| LimitNoOrderError
|
||||||
| NotFound
|
| NotFound
|
||||||
| NoRelBetween Text Text Text
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [ProcDescription]
|
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [ProcDescription]
|
||||||
| NotEmbedded Text
|
| NotEmbedded Text
|
||||||
| ParseRequestError Text Text
|
| ParseRequestError Text Text
|
||||||
|
|||||||
+55
-5
@@ -18,6 +18,7 @@ module PostgREST.Error
|
|||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.FuzzySet as Fuzzy
|
import qualified Data.FuzzySet as Fuzzy
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Text.Encoding.Error as T
|
import qualified Data.Text.Encoding.Error as T
|
||||||
@@ -36,12 +37,14 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
|||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import qualified PostgREST.MediaType as MediaType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
|
Schema)
|
||||||
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
ProcParam (..))
|
ProcParam (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..),
|
||||||
|
RelationshipsMap)
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
@@ -152,11 +155,12 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON (NoRelBetween parent child schema) = JSON.object [
|
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode00,
|
"code" .= SchemaCacheErrorCode00,
|
||||||
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= ("Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."),
|
||||||
"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)]
|
"hint" .= noRelBetweenHint parent child schema allRels]
|
||||||
|
|
||||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode01,
|
"code" .= SchemaCacheErrorCode01,
|
||||||
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||||
@@ -191,6 +195,52 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- If no relationship is found then:
|
||||||
|
--
|
||||||
|
-- Looks for parent suggestions if parent not found
|
||||||
|
-- Looks for child suggestions if parent is found but child is not
|
||||||
|
-- Gives no suggestions if both are found (it means that there is a problem with the embed hint)
|
||||||
|
--
|
||||||
|
-- >>> :set -Wno-missing-fields
|
||||||
|
-- >>> let qi t = QualifiedIdentifier "api" t
|
||||||
|
-- >>> let rel ft = Relationship{relForeignTable = qi ft}
|
||||||
|
-- >>> let rels = HM.fromList [((qi "films", "api"), [rel "directors", rel "roles", rel "actors"])]
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "film" "directors" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'films' instead of 'film'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "noclosealternative" "roles" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "noclosealternative" "api" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "noclosealternative" "noclosealternative" rels
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
noRelBetweenHint :: Text -> Text -> Schema -> RelationshipsMap -> Maybe Text
|
||||||
|
noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
|
||||||
|
if isJust findParent
|
||||||
|
then (<> "' instead of '" <> child <> "'.") <$> suggestChild
|
||||||
|
else (<> "' instead of '" <> parent <> "'.") <$> suggestParent
|
||||||
|
where
|
||||||
|
findParent = HM.lookup (QualifiedIdentifier schema parent, schema) allRels
|
||||||
|
fuzzySetOfParents = Fuzzy.fromList [qiName (fst p) | p <- HM.keys allRels, snd p == schema]
|
||||||
|
fuzzySetOfChildren = Fuzzy.fromList [qiName (relForeignTable c) | c <- fromMaybe [] findParent]
|
||||||
|
suggestParent = Fuzzy.getOne fuzzySetOfParents parent
|
||||||
|
-- Do not give suggestion if the child is found in the relations (weight = 1.0)
|
||||||
|
suggestChild = headMay [snd k | k <- Fuzzy.get fuzzySetOfChildren child, fst k < 1.0]
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- If no function is found with the given name, it does a fuzzy search to all the functions
|
-- If no function is found with the given name, it does a fuzzy search to all the functions
|
||||||
-- in the same schema and shows the best match as hint.
|
-- in the same schema and shows the best match as hint.
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
|
|||||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||||
findRel schema allRels origin target hint =
|
findRel schema allRels origin target hint =
|
||||||
case rels of
|
case rels of
|
||||||
[] -> Left $ NoRelBetween origin target schema
|
[] -> Left $ NoRelBetween origin target hint schema allRels
|
||||||
[r] -> Right r
|
[r] -> Right r
|
||||||
rs -> Left $ AmbiguousRelBetween origin target rs
|
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -202,10 +202,10 @@ spec =
|
|||||||
it "fails if the fk is not known" $
|
it "fails if the fk is not known" $
|
||||||
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
||||||
[json|{
|
[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.",
|
"hint":null,
|
||||||
"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",
|
"code": "PGRST200",
|
||||||
"details": null}|]
|
"details":"Searched for a foreign key relationship between 'message' and 'person' using the hint 'space' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
@@ -492,10 +492,10 @@ spec =
|
|||||||
it "doesn't work if the junction is only internal" $
|
it "doesn't work if the junction is only internal" $
|
||||||
get "/end_1?select=end_2(*)" `shouldRespondWith`
|
get "/end_1?select=end_2(*)" `shouldRespondWith`
|
||||||
[json|{
|
[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.",
|
"hint": null,
|
||||||
"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",
|
"code":"PGRST200",
|
||||||
"details": null}|]
|
"details": "Searched for a foreign key relationship between 'end_1' and 'end_2' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
it "shouldn't try to embed if the private junction has an exposed homonym" $
|
it "shouldn't try to embed if the private junction has an exposed homonym" $
|
||||||
@@ -503,10 +503,10 @@ spec =
|
|||||||
-- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669
|
-- Ref: https://github.com/PostgREST/postgrest/issues/1587#issuecomment-734995669
|
||||||
get "/schauspieler?select=filme(*)" `shouldRespondWith`
|
get "/schauspieler?select=filme(*)" `shouldRespondWith`
|
||||||
[json|{
|
[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.",
|
"hint":null,
|
||||||
"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",
|
"code":"PGRST200",
|
||||||
"details": null}|]
|
"details":"Searched for a foreign key relationship between 'schauspieler' and 'filme' in the schema 'test', but no matches were found."}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson] }
|
, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
|||||||
@@ -590,8 +590,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request partitions as children from a partitioned table" $
|
it "cannot request partitions as children from a partitioned table" $
|
||||||
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
|
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[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.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_models' and 'car_model_sales_202101' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -601,8 +601,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request a partitioned table as parent from a partition" $
|
it "cannot request a partitioned table as parent from a partition" $
|
||||||
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[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.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_model_sales_202101' and 'car_models' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -612,8 +612,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request a partition as parent from a partitioned table" $
|
it "cannot request a partition as parent from a partitioned table" $
|
||||||
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[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.",
|
{"hint":"Perhaps you meant 'car_models' instead of 'car_models_default'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_model_sales' and 'car_models_default' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
@@ -623,8 +623,8 @@ spec actualPgVersion = do
|
|||||||
it "cannot request partitioned tables as children from a partition" $
|
it "cannot request partitioned tables as children from a partition" $
|
||||||
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
|
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
[json|
|
[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.",
|
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_models_default'.",
|
||||||
"details":null,
|
"details":"Searched for a foreign key relationship between 'car_models_default' and 'car_model_sales' in the schema 'test', but no matches were found.",
|
||||||
"code":"PGRST200",
|
"code":"PGRST200",
|
||||||
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
|
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
|
|||||||
Reference in New Issue
Block a user