Compare commits
11
Commits
feat/openapi
...
v10.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f56bed2a75 | ||
|
|
af8e436732 | ||
|
|
98a29bee04 | ||
|
|
557285b659 | ||
|
|
81501aefa0 | ||
|
|
12c1d4a8e4 | ||
|
|
8aa7368786 | ||
|
|
9d4ff812c9 | ||
|
|
171dd313d9 | ||
|
|
fd24a7374b | ||
|
|
a525790c4c |
+13
-1
@@ -3,7 +3,19 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
## [10.1.2] - 2023-02-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- #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
|
||||
- #2582, Move explanation about "single parameters" from the `message` to the `details` in the error output - @laurenceisla
|
||||
- #2569, Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions - @laurenceisla
|
||||
- #1405, Add the required OpenAPI items object when the parameter is an array - @laurenceisla
|
||||
- #2592, Add upsert headers for POST requests to the OpenAPI output - @laurenceisla
|
||||
- #2623, Fix FK pointing to VIEW instead of TABLE in OpenAPI output - @laurenceisla
|
||||
- #2622, Consider any PostgreSQL authentication failure as fatal and exit immediately - @michivi
|
||||
- #2620, Fix `NOTIFY pgrst` not reloading the db connections catalog cache - @steve-chavez
|
||||
|
||||
## [10.1.1] - 2022-11-08
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ let
|
||||
export PGDATABASE
|
||||
export PGRST_DB_SCHEMAS
|
||||
|
||||
HBA_FILE="$tmpdir/pg_hba.conf"
|
||||
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
|
||||
echo "local $PGDATABASE all trust" >> "$HBA_FILE"
|
||||
|
||||
log "Initializing database cluster..."
|
||||
# We try to make the database cluster as independent as possible from the host
|
||||
# by specifying the timezone, locale and encoding.
|
||||
@@ -62,7 +66,7 @@ let
|
||||
|
||||
log "Starting the database cluster..."
|
||||
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -k $PGHOST -c log_statement=\"all\"" \
|
||||
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\"" \
|
||||
>> "$setuplog"
|
||||
|
||||
stop () {
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 10.1.1
|
||||
version: 10.1.2
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
@@ -85,6 +85,7 @@ library
|
||||
, contravariant-extras >= 0.3.3 && < 0.4
|
||||
, cookie >= 0.4.2 && < 0.5
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, fuzzyset >= 0.2.3
|
||||
, gitrev >= 1.2 && < 1.4
|
||||
, hasql >= 1.6.1.1 && < 1.7
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
|
||||
@@ -453,7 +453,7 @@ requestMediaTypes conf action path =
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||
case matchProc of
|
||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost
|
||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||
([], [proc]) -> Right proc
|
||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||
@@ -461,7 +461,9 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPo
|
||||
([proc], _) -> Right proc
|
||||
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
||||
where
|
||||
matchProc = overloadedProcPartition $ HM.lookupDefault mempty qi allProcs -- first find the proc by name
|
||||
matchProc = overloadedProcPartition lookupProcName
|
||||
-- First find the proc by name
|
||||
lookupProcName = HM.lookupDefault mempty qi allProcs
|
||||
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
||||
-- where fallbackProcs are functions with a single unnamed parameter
|
||||
overloadedProcPartition = foldr select ([],[])
|
||||
|
||||
@@ -32,9 +32,11 @@ module PostgREST.ApiRequest.Types
|
||||
) where
|
||||
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier)
|
||||
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
||||
import PostgREST.SchemaCache.Relationship (Relationship)
|
||||
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||
RelationshipsMap)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -64,8 +66,8 @@ data ApiRequestError
|
||||
| InvalidRpcMethod ByteString
|
||||
| LimitNoOrderError
|
||||
| NotFound
|
||||
| NoRelBetween Text Text Text
|
||||
| NoRpc Text Text [Text] Bool MediaType Bool
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [ProcDescription]
|
||||
| NotEmbedded Text
|
||||
| ParseRequestError Text Text
|
||||
| PutRangeNotAllowedError
|
||||
|
||||
+130
-19
@@ -17,6 +17,8 @@ module PostgREST.Error
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.FuzzySet as Fuzzy
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Data.Text.Encoding.Error as T
|
||||
@@ -35,12 +37,14 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||
ProcParam (..))
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..))
|
||||
Relationship (..),
|
||||
RelationshipsMap)
|
||||
import Protolude
|
||||
|
||||
|
||||
@@ -151,36 +155,143 @@ instance JSON.ToJSON ApiRequestError where
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (NoRelBetween parent child schema) = JSON.object [
|
||||
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
||||
"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)]
|
||||
"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" .= noRelBetweenHint parent child schema allRels]
|
||||
|
||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode01,
|
||||
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||
"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 [
|
||||
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
|
||||
let func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
prmsMsg = "(" <> prms <> ")"
|
||||
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
|
||||
in JSON.object [
|
||||
"code" .= SchemaCacheErrorCode02,
|
||||
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||
"message" .= ("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"),
|
||||
"details" .= ("Searched for the function " <> func <>
|
||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||
(_, True, MTTextPlain) -> " function with a single unnamed text parameter"
|
||||
(_, True, MTTextXML) -> " function with a single unnamed xml parameter"
|
||||
(_, True, MTOctetStream) -> " function with a single unnamed bytea parameter"
|
||||
(_, True, MTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
||||
_ -> prms <> " function") <>
|
||||
" 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)]
|
||||
(True, _, _) -> " with a single json/jsonb parameter"
|
||||
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||
(_, True, MTTextXML) -> " with a single unnamed xml parameter"
|
||||
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||
_ -> fmtPrms prmsDet) <>
|
||||
", but no matches were found in the schema cache."),
|
||||
-- The hint will be null in the case of single unnamed parameter functions
|
||||
"hint" .= if onlySingleParams
|
||||
then Nothing
|
||||
else noRpcHint schema procName argumentKeys allProcs overloadedProcs ]
|
||||
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)]
|
||||
|
||||
-- |
|
||||
-- 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
|
||||
-- in the same schema and shows the best match as hint.
|
||||
--
|
||||
-- >>> :set -Wno-missing-fields
|
||||
-- >>> let procs = [(QualifiedIdentifier "api" "test"), (QualifiedIdentifier "api" "another"), (QualifiedIdentifier "private" "other")]
|
||||
--
|
||||
-- >>> noRpcHint "api" "testt" ["val", "param", "name"] procs []
|
||||
-- Just "Perhaps you meant to call the function api.test"
|
||||
--
|
||||
-- >>> noRpcHint "api" "other" [] procs []
|
||||
-- Just "Perhaps you meant to call the function api.another"
|
||||
--
|
||||
-- >>> noRpcHint "api" "noclosealternative" [] procs []
|
||||
-- Nothing
|
||||
--
|
||||
-- If a function is found with the given name, but no params match, then it does a fuzzy search
|
||||
-- to all the overloaded functions' params using the form "param1, param2, param3, ..."
|
||||
-- and shows the best match as hint.
|
||||
--
|
||||
-- >>> let procsDesc = [ProcDescription {pdParams = [ProcParam {ppName="val"}, ProcParam {ppName="param"}, ProcParam {ppName="name"}]}, ProcDescription {pdParams = [ProcParam {ppName="id"}, ProcParam {ppName="attr"}]}]
|
||||
--
|
||||
-- >>> noRpcHint "api" "test" ["vall", "pqaram", "nam"] procs procsDesc
|
||||
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
||||
--
|
||||
-- >>> noRpcHint "api" "test" ["val", "param"] procs procsDesc
|
||||
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
||||
--
|
||||
-- >>> noRpcHint "api" "test" ["id", "attrs"] procs procsDesc
|
||||
-- Just "Perhaps you meant to call the function api.test(attr, id)"
|
||||
--
|
||||
-- >>> noRpcHint "api" "test" ["id"] procs procsDesc
|
||||
-- Just "Perhaps you meant to call the function api.test(attr, id)"
|
||||
--
|
||||
-- >>> noRpcHint "api" "test" ["noclosealternative"] procs procsDesc
|
||||
-- Nothing
|
||||
--
|
||||
noRpcHint :: Text -> Text -> [Text] -> [QualifiedIdentifier] -> [ProcDescription] -> Maybe Text
|
||||
noRpcHint schema procName params allProcs overloadedProcs =
|
||||
fmap (("Perhaps you meant to call the function " <> schema <> ".") <>) possibleProcs
|
||||
where
|
||||
fuzzySetOfProcs = Fuzzy.fromList [qiName k | k <- allProcs, qiSchema k == schema]
|
||||
fuzzySetOfParams = Fuzzy.fromList $ listToText <$> [[ppName prm | prm <- pdParams ov] | ov <- overloadedProcs]
|
||||
-- Cannot do a fuzzy search like: Fuzzy.getOne [[Text]] [Text], where [[Text]] is the list of params for each
|
||||
-- overloaded function and [Text] the given params. This converts those lists to text to make fuzzy search possible.
|
||||
-- E.g. ["val", "param", "name"] into "(name, param, val)"
|
||||
listToText = ("(" <>) . (<> ")") . T.intercalate ", " . sort
|
||||
possibleProcs
|
||||
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
|
||||
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
|
||||
|
||||
compressedRel :: Relationship -> JSON.Value
|
||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||
compressedRel ComputedRelationship{} = JSON.object mempty
|
||||
@@ -193,7 +304,7 @@ compressedRel Relationship{..} =
|
||||
: case relCardinality of
|
||||
M2M Junction{..} -> [
|
||||
"cardinality" .= ("many-to-many" :: Text)
|
||||
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColumns2))
|
||||
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColsSource) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColsTarget))
|
||||
]
|
||||
M2O cons relColumns -> [
|
||||
"cardinality" .= ("many-to-one" :: Text)
|
||||
@@ -317,7 +428,7 @@ checkIsFatal :: PgError -> Maybe Text
|
||||
checkIsFatal (PgError _ (SQL.ConnectionUsageError e))
|
||||
| isAuthFailureMessage = Just $ toS failureMessage
|
||||
| otherwise = Nothing
|
||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` failureMessage
|
||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isInfixOf` failureMessage
|
||||
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||
checkIsFatal (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError))))
|
||||
= case serverError of
|
||||
|
||||
@@ -196,7 +196,7 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
|
||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||
findRel schema allRels origin target hint =
|
||||
case rels of
|
||||
[] -> Left $ NoRelBetween origin target schema
|
||||
[] -> Left $ NoRelBetween origin target hint schema allRels
|
||||
[r] -> Right r
|
||||
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||
where
|
||||
@@ -357,7 +357,7 @@ mutatePlan mutation qi ApiRequest{..} sCache readReq = mapLeft ApiRequestError $
|
||||
returnings =
|
||||
if iPreferRepresentation == None
|
||||
then []
|
||||
else returningCols readReq pkCols
|
||||
else inferColsEmbedNeeds readReq pkCols
|
||||
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
||||
logic = map snd qsLogic
|
||||
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
||||
@@ -371,7 +371,7 @@ callPlan proc apiReq readReq = FunctionCall {
|
||||
, funCArgs = payRaw <$> iPayload apiReq
|
||||
, funCScalar = procReturnsScalar proc
|
||||
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||
, funCReturning = returningCols readReq []
|
||||
, funCReturning = inferColsEmbedNeeds readReq []
|
||||
}
|
||||
where
|
||||
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||
@@ -382,14 +382,15 @@ callPlan proc apiReq readReq = FunctionCall {
|
||||
prms -> KeyParams $ specifiedParams prms
|
||||
specifiedParams = filter (\x -> ppName x `S.member` iColumns apiReq)
|
||||
|
||||
returningCols :: ReadPlanTree -> [FieldName] -> [FieldName]
|
||||
returningCols rr@(Node _ forest) pkCols
|
||||
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
||||
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
|
||||
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
-- if * is part of the select, we must not add pk or fk columns manually -
|
||||
-- otherwise those would be selected and output twice
|
||||
| "*" `elem` fldNames = ["*"]
|
||||
| otherwise = returnings
|
||||
where
|
||||
fldNames = fstFieldNames rr
|
||||
fldNames = (\((fld, _), _, _) -> fld) <$> select
|
||||
-- Without fkCols, when a mutatePlan to
|
||||
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||
@@ -403,8 +404,8 @@ returningCols rr@(Node _ forest) pkCols
|
||||
Just $ fst <$> cols
|
||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2O _ cols}} _ ->
|
||||
Just $ fst <$> cols
|
||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2M Junction{junColumns1, junColumns2}}} _ ->
|
||||
Just $ (fst <$> junColumns1) ++ (fst <$> junColumns2)
|
||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2M Junction{junColsSource=cols}}} _ ->
|
||||
Just $ fst <$> cols
|
||||
Node ReadPlan{relToParent=Just ComputedRelationship{}} _ ->
|
||||
Nothing
|
||||
Node ReadPlan{relToParent=Nothing} _ ->
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
module PostgREST.Plan.ReadPlan
|
||||
( ReadPlanTree
|
||||
, ReadPlan(..)
|
||||
, fstFieldNames
|
||||
, JoinCondition(..)
|
||||
) where
|
||||
|
||||
@@ -45,8 +43,3 @@ data ReadPlan = ReadPlan
|
||||
-- ^ used for aliasing
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
||||
fstFieldNames :: ReadPlanTree -> [FieldName]
|
||||
fstFieldNames (Node ReadPlan{select} _) =
|
||||
fst . (\(f, _, _) -> f) <$> select
|
||||
|
||||
@@ -66,10 +66,16 @@ toSwaggerType "bigint" = Just SwaggerInteger
|
||||
toSwaggerType "numeric" = Just SwaggerNumber
|
||||
toSwaggerType "real" = Just SwaggerNumber
|
||||
toSwaggerType "double precision" = Just SwaggerNumber
|
||||
toSwaggerType "ARRAY" = Just SwaggerArray
|
||||
toSwaggerType "json" = Nothing
|
||||
toSwaggerType "jsonb" = Nothing
|
||||
toSwaggerType _ = Just SwaggerString
|
||||
toSwaggerType colType = case T.takeEnd 2 colType of
|
||||
"[]" -> Just SwaggerArray
|
||||
_ -> Just SwaggerString
|
||||
|
||||
makeSwaggerItemType :: Maybe (SwaggerType t) -> Text -> Maybe (Referenced Schema)
|
||||
makeSwaggerItemType itemType colType = case itemType of
|
||||
Just SwaggerArray -> Just $ Inline (mempty & type_ .~ toSwaggerType (T.dropEnd 2 colType))
|
||||
_ -> Nothing
|
||||
|
||||
parseDefault :: Text -> Text -> Text
|
||||
parseDefault colType colDefault =
|
||||
@@ -97,11 +103,14 @@ makeProperty tbl rels col = (colName col, Inline s)
|
||||
fk :: Maybe Text
|
||||
fk =
|
||||
let
|
||||
searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
||||
-- Sorts the relationship list to get tables first
|
||||
relsSortedByIsView = sortOn relFTableIsView [ r | r@Relationship{} <- searchedRels]
|
||||
-- Finds the relationship that has a single column foreign key
|
||||
rel = find (\case
|
||||
Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
|
||||
_ -> False
|
||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
||||
) relsSortedByIsView
|
||||
fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
|
||||
fTbl = qiName . relForeignTable <$> rel
|
||||
fTblCol = (,) <$> fTbl <*> fCol
|
||||
@@ -119,6 +128,7 @@ makeProperty tbl rels col = (colName col, Inline s)
|
||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||
else
|
||||
colDescription col
|
||||
pType = toSwaggerType (colType col)
|
||||
s =
|
||||
(mempty :: Schema)
|
||||
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
||||
@@ -126,7 +136,8 @@ makeProperty tbl rels col = (colName col, Inline s)
|
||||
& enum_ .~ e
|
||||
& format ?~ colType col
|
||||
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
||||
& type_ .~ toSwaggerType (colType col)
|
||||
& type_ .~ pType
|
||||
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType pType (colType col))
|
||||
|
||||
makeProcSchema :: ProcDescription -> Schema
|
||||
makeProcSchema pd =
|
||||
@@ -141,6 +152,7 @@ makeProcProperty (ProcParam n t _ _) = (n, Inline s)
|
||||
where
|
||||
s = (mempty :: Schema)
|
||||
& type_ .~ toSwaggerType t
|
||||
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType (toSwaggerType t) t)
|
||||
& format ?~ t
|
||||
|
||||
makePreferParam :: [Text] -> Param
|
||||
@@ -152,7 +164,15 @@ makePreferParam ts =
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ ?~ SwaggerString
|
||||
& enum_ .~ JSON.decode (JSON.encode ts))
|
||||
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
|
||||
where
|
||||
val :: Text -> [Text]
|
||||
val = \case
|
||||
"count" -> ["count=none"]
|
||||
"params" -> ["params=single-object"]
|
||||
"return" -> ["return=representation", "return=minimal", "return=none"]
|
||||
"resolution" -> ["resolution=ignore-duplicates", "resolution=merge-duplicates"]
|
||||
_ -> []
|
||||
|
||||
makeProcParam :: ProcDescription -> [Referenced Param]
|
||||
makeProcParam pd =
|
||||
@@ -165,9 +185,11 @@ makeProcParam pd =
|
||||
|
||||
makeParamDefs :: [Table] -> [(Text, Param)]
|
||||
makeParamDefs ti =
|
||||
[ ("preferParams", makePreferParam ["params=single-object"])
|
||||
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
|
||||
, ("preferCount", makePreferParam ["count=none"])
|
||||
-- TODO: create Prefer for each method (GET, PATCH, etc.)
|
||||
[ ("preferParams", makePreferParam ["params"])
|
||||
, ("preferReturn", makePreferParam ["return"])
|
||||
, ("preferCount", makePreferParam ["count"])
|
||||
, ("preferPost", makePreferParam ["return", "resolution"])
|
||||
, ("select", (mempty :: Param)
|
||||
& name .~ "select"
|
||||
& description ?~ "Filtering Columns"
|
||||
@@ -267,7 +289,7 @@ makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t
|
||||
)
|
||||
)
|
||||
postOp = tOp
|
||||
& parameters .~ fmap ref ["body." <> tn, "select", "preferReturn"]
|
||||
& parameters .~ fmap ref ["body." <> tn, "select", "preferPost"]
|
||||
& at 201 ?~ "Created"
|
||||
patchOp = tOp
|
||||
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"])
|
||||
|
||||
@@ -512,13 +512,11 @@ tablesSqlQuery pgVer =
|
||||
CASE
|
||||
WHEN t.typtype = 'd' THEN
|
||||
CASE
|
||||
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text
|
||||
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
|
||||
ELSE format_type(a.atttypid, a.atttypmod)
|
||||
END
|
||||
ELSE
|
||||
CASE
|
||||
WHEN t.typelem <> 0::oid AND t.typlen = (-1) THEN 'ARRAY'::text
|
||||
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
|
||||
ELSE format_type(a.atttypid, a.atttypmod)
|
||||
END
|
||||
|
||||
@@ -55,8 +55,8 @@ data Junction = Junction
|
||||
{ junTable :: QualifiedIdentifier
|
||||
, junConstraint1 :: FKConstraint
|
||||
, junConstraint2 :: FKConstraint
|
||||
, junColumns1 :: [(FieldName, FieldName)]
|
||||
, junColumns2 :: [(FieldName, FieldName)]
|
||||
, junColsSource :: [(FieldName, FieldName)]
|
||||
, junColsTarget :: [(FieldName, FieldName)]
|
||||
}
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
|
||||
@@ -230,16 +230,15 @@ listener appState = do
|
||||
listener appState
|
||||
|
||||
handleNotification _ msg
|
||||
| BS.null msg = scLoader -- reload the schema cache
|
||||
| msg == "reload schema" = scLoader -- reload the schema cache
|
||||
| msg == "reload config" = reReadConfig False appState -- reload the config
|
||||
| BS.null msg = cacheReloader
|
||||
| msg == "reload schema" = cacheReloader
|
||||
| msg == "reload config" = reReadConfig False appState
|
||||
| otherwise = pure () -- Do nothing if anything else than an empty message is sent
|
||||
|
||||
scLoader =
|
||||
-- It's not necessary to check the loadSchemaCache success
|
||||
-- here. If the connection drops, the thread will die and
|
||||
-- proceed to recover.
|
||||
void $ loadSchemaCache appState
|
||||
cacheReloader =
|
||||
-- reloads the schema cache + restarts pool connections
|
||||
-- it's necessary to restart the pg connections because they cache the pg catalog(see #2620)
|
||||
connectionWorker appState
|
||||
|
||||
-- | Re-reads the config plus config options from the db
|
||||
reReadConfig :: Bool -> AppState -> IO ()
|
||||
|
||||
@@ -15,4 +15,5 @@ main =
|
||||
, "src/PostgREST/Query/SqlFragment.hs"
|
||||
, "src/PostgREST/ApiRequest/Preferences.hs"
|
||||
, "src/PostgREST/ApiRequest/QueryParams.hs"
|
||||
, "src/PostgREST/Error.hs"
|
||||
]
|
||||
|
||||
@@ -86,3 +86,15 @@ $$ language sql;
|
||||
create or replace function hello() returns text as $$
|
||||
select 'hello';
|
||||
$$ language sql;
|
||||
|
||||
create table cats(id uuid primary key, name text);
|
||||
grant all on cats to postgrest_test_anonymous;
|
||||
|
||||
create function drop_change_cats() returns void
|
||||
language sql security definer
|
||||
as $$
|
||||
drop table cats;
|
||||
create table cats(id bigint primary key, name text);
|
||||
grant all on table cats to postgrest_test_anonymous;
|
||||
notify pgrst, 'reload schema';
|
||||
$$;
|
||||
|
||||
+11
-1
@@ -50,6 +50,7 @@ def run(
|
||||
env=None,
|
||||
port=None,
|
||||
host=None,
|
||||
wait_for_readiness=True,
|
||||
no_pool_connection_available=False,
|
||||
):
|
||||
"Run PostgREST and yield an endpoint that is ready for connections."
|
||||
@@ -88,7 +89,8 @@ def run(
|
||||
process.stdin.write(stdin or b"")
|
||||
process.stdin.close()
|
||||
|
||||
wait_until_ready(adminurl + "/ready")
|
||||
if wait_for_readiness:
|
||||
wait_until_ready(adminurl + "/ready")
|
||||
|
||||
process.stdout.read()
|
||||
|
||||
@@ -137,6 +139,14 @@ def freeport():
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def wait_until_exit(postgrest):
|
||||
"Wait for PostgREST to exit, or times out"
|
||||
try:
|
||||
return postgrest.process.wait(timeout=1)
|
||||
except (subprocess.TimeoutExpired):
|
||||
raise PostgrestTimedOut()
|
||||
|
||||
|
||||
def wait_until_ready(url):
|
||||
"Wait for the given HTTP endpoint to return a status of 200."
|
||||
session = requests_unixsocket.Session()
|
||||
|
||||
+31
-1
@@ -66,6 +66,15 @@ def test_read_secret_from_stdin_dbconfig(defaultenv):
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_fail_with_invalid_password(defaultenv):
|
||||
"Connecting with an invalid password should fail without retries."
|
||||
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
|
||||
env = {**defaultenv, "PGRST_DB_URI": uri}
|
||||
with run(env=env, wait_for_readiness=False) as postgrest:
|
||||
exitCode = wait_until_exit(postgrest)
|
||||
assert exitCode == 1
|
||||
|
||||
|
||||
def test_connect_with_dburi(dburi, defaultenv):
|
||||
"Connecting with db-uri instead of LIPQ* environment variables should work."
|
||||
defaultenv_without_libpq = {
|
||||
@@ -325,7 +334,7 @@ def test_db_schema_notify_reload(defaultenv):
|
||||
"/rpc/change_db_schema_and_full_reload", data={"schemas": "v1"}
|
||||
)
|
||||
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.2)
|
||||
|
||||
response = postgrest.session.get("/rpc/get_guc_value?name=search_path")
|
||||
assert response.text == '"\\"v1\\", \\"public\\""'
|
||||
@@ -818,6 +827,27 @@ def test_no_pool_connection_required_on_bad_embedding(defaultenv):
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_notify_reloading_catalog_cache(defaultenv):
|
||||
"notify should reload the connection catalog cache"
|
||||
|
||||
with run(env=defaultenv) as postgrest:
|
||||
|
||||
# first the id col is an uuid
|
||||
response = postgrest.session.get(
|
||||
"/cats?id=eq.dea27321-f988-4a57-93e4-8eeb38f3cf1e"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# change it to a bigint
|
||||
response = postgrest.session.post("/rpc/drop_change_cats")
|
||||
assert response.status_code == 204
|
||||
time.sleep(0.1)
|
||||
|
||||
# next request should succeed with a bigint value
|
||||
response = postgrest.session.get("/cats?id=eq.1")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
|
||||
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
|
||||
# A stack size of 200K seems to be enough for succeess
|
||||
|
||||
@@ -91,7 +91,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
[
|
||||
{ "$ref": "#/parameters/body.child_entities" },
|
||||
{ "$ref": "#/parameters/select" },
|
||||
{ "$ref": "#/parameters/preferReturn" }
|
||||
{ "$ref": "#/parameters/preferPost" }
|
||||
]
|
||||
|]
|
||||
|
||||
@@ -310,6 +310,23 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
}
|
||||
|]
|
||||
|
||||
describe "VIEW created for a TABLE with a O2M relationship" $ do
|
||||
|
||||
it "fk points to destination TABLE instead of the VIEW" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let referralLink = r ^? key "definitions" . key "projects" . key "properties" . key "client_id"
|
||||
|
||||
liftIO $
|
||||
referralLink `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "integer",
|
||||
"type": "integer",
|
||||
"description": "Note:\nThis is a Foreign Key to `clients.id`.<fk table='clients' column='id'/>"
|
||||
}
|
||||
|]
|
||||
|
||||
describe "PostgreSQL to Swagger Type Mapping" $ do
|
||||
|
||||
it "character varying to string" $ do
|
||||
@@ -490,6 +507,117 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
}
|
||||
|]
|
||||
|
||||
it "array types to array" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let text_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text_arr"
|
||||
let int_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_int_arr"
|
||||
let bool_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bool_arr"
|
||||
let char_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_char_arr"
|
||||
let varchar_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_varchar_arr"
|
||||
let bigint_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint_arr"
|
||||
let numeric_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric_arr"
|
||||
let json_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_json_arr"
|
||||
let jsonb_arr_types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_jsonb_arr"
|
||||
|
||||
liftIO $ do
|
||||
|
||||
text_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "text[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
int_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "integer[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
bool_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "boolean[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
char_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "character[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
varchar_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "character varying[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
bigint_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "bigint[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
numeric_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "numeric[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
|]
|
||||
|
||||
json_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "json[]",
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
|]
|
||||
|
||||
jsonb_arr_types `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"format": "jsonb[]",
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
|]
|
||||
|
||||
|
||||
describe "Detects default values" $ do
|
||||
|
||||
it "text" $ do
|
||||
@@ -569,7 +697,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
it "includes function summary/description and body schema for arguments" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let method s = key "paths" . key "/rpc/varied_arguments" . key s
|
||||
let method s = key "paths" . key "/rpc/varied_arguments_openapi" . key s
|
||||
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
|
||||
summary = r ^? method "post" . key "summary"
|
||||
description = r ^? method "post" . key "description"
|
||||
@@ -590,7 +718,15 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
"date",
|
||||
"money",
|
||||
"enum",
|
||||
"arr"
|
||||
"text_arr",
|
||||
"int_arr",
|
||||
"bool_arr",
|
||||
"char_arr",
|
||||
"varchar_arr",
|
||||
"bigint_arr",
|
||||
"numeric_arr",
|
||||
"json_arr",
|
||||
"jsonb_arr"
|
||||
],
|
||||
"properties": {
|
||||
"double": {
|
||||
@@ -617,9 +753,64 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
||||
"format": "enum_menagerie_type",
|
||||
"type": "string"
|
||||
},
|
||||
"arr": {
|
||||
"text_arr": {
|
||||
"format": "text[]",
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"int_arr": {
|
||||
"format": "integer[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"bool_arr": {
|
||||
"format": "boolean[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"char_arr": {
|
||||
"format": "character[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"varchar_arr": {
|
||||
"format": "character varying[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bigint_arr": {
|
||||
"format": "bigint[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"numeric_arr": {
|
||||
"format": "numeric[]",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"json_arr": {
|
||||
"format": "json[]",
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"jsonb_arr": {
|
||||
"format": "jsonb[]",
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"integer": {
|
||||
"format": "integer",
|
||||
|
||||
@@ -202,10 +202,10 @@ spec =
|
||||
it "fails if the fk is not known" $
|
||||
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.",
|
||||
"hint":null,
|
||||
"message":"Could not find a relationship between 'message' and 'person' in the schema cache",
|
||||
"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
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -492,10 +492,10 @@ spec =
|
||||
it "doesn't work if the junction is only internal" $
|
||||
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.",
|
||||
"hint": null,
|
||||
"message":"Could not find a relationship between 'end_1' and 'end_2' in the schema cache",
|
||||
"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
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
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
|
||||
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.",
|
||||
"hint":null,
|
||||
"message":"Could not find a relationship between 'schauspieler' and 'filme' in the schema cache",
|
||||
"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
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
|
||||
@@ -590,8 +590,8 @@ spec actualPgVersion = do
|
||||
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`
|
||||
[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,
|
||||
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||
"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",
|
||||
"message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |]
|
||||
{ matchStatus = 400
|
||||
@@ -601,8 +601,8 @@ spec actualPgVersion = do
|
||||
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`
|
||||
[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,
|
||||
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.",
|
||||
"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",
|
||||
"message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |]
|
||||
{ matchStatus = 400
|
||||
@@ -612,8 +612,8 @@ spec actualPgVersion = do
|
||||
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`
|
||||
[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,
|
||||
{"hint":"Perhaps you meant 'car_models' instead of 'car_models_default'.",
|
||||
"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",
|
||||
"message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |]
|
||||
{ matchStatus = 400
|
||||
@@ -623,8 +623,8 @@ spec actualPgVersion = do
|
||||
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`
|
||||
[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,
|
||||
{"hint":"Perhaps you meant 'car_model_sales' instead of 'car_models_default'.",
|
||||
"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",
|
||||
"message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |]
|
||||
{ matchStatus = 400
|
||||
|
||||
@@ -120,17 +120,39 @@ spec actualPgVersion =
|
||||
it "should fail with 404 on unknown proc name" $
|
||||
get "/rpc/fake" `shouldRespondWith` 404
|
||||
|
||||
it "should fail with 404 and hint the closest proc on unknown proc name" $
|
||||
get "/rpc/sayhell" `shouldRespondWith`
|
||||
[json| {
|
||||
"hint":"Perhaps you meant to call the function test.sayhello",
|
||||
"message":"Could not find the function test.sayhell without parameters in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "should fail with 404 on unknown proc args" $ do
|
||||
get "/rpc/sayhello" `shouldRespondWith` 404
|
||||
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
|
||||
|
||||
it "should fail with 404 and hint the closest args on unknown proc args" $
|
||||
get "/rpc/sayhello?nam=Peter" `shouldRespondWith`
|
||||
[json| {
|
||||
"hint":"Perhaps you meant to call the function test.sayhello(name)",
|
||||
"message":"Could not find the function test.sayhello(nam) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":"Searched for the function test.sayhello with parameter nam, but no matches were found in the schema cache."} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "should not ignore unknown args and fail with 404" $
|
||||
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",
|
||||
"hint":"Perhaps you meant to call the function test.add_them(a, b)",
|
||||
"message":"Could not find the function test.add_them(a, b, smthelse) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null} |]
|
||||
"details":"Searched for the function test.add_them with parameters a, b, smthelse, but no matches were found in the schema cache."} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
@@ -141,10 +163,10 @@ spec actualPgVersion =
|
||||
[json|{}|]
|
||||
`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",
|
||||
"hint":null,
|
||||
"message":"Could not find the function test.sayhello in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null} |]
|
||||
"details":"Searched for the function test.sayhello with a single json/jsonb parameter, but no matches were found in the schema cache."} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
@@ -152,19 +174,19 @@ spec actualPgVersion =
|
||||
it "should fail with 404 for overloaded functions with unknown args" $ do
|
||||
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",
|
||||
"hint":null,
|
||||
"message":"Could not find the function test.overloaded(wrong_arg) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null} |]
|
||||
"details":"Searched for the function test.overloaded with parameter wrong_arg, but no matches were found in the schema cache."} |]
|
||||
{ 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",
|
||||
"hint":"Perhaps you meant to call the function test.overloaded(a, b, c)",
|
||||
"message":"Could not find the function test.overloaded(a, b, wrong_arg) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null} |]
|
||||
"details":"Searched for the function test.overloaded with parameters a, b, wrong_arg, but no matches were found in the schema cache."} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
@@ -246,13 +268,17 @@ spec actualPgVersion =
|
||||
`shouldRespondWith`
|
||||
[json|{"id": 2, "articleStars": [{"userId": 3}]}|]
|
||||
|
||||
it "can embed an M2M relationship table" $
|
||||
it "can embed an M2M relationship table" $ do
|
||||
get "/rpc/getallusers?select=name,tasks(name)&id=gt.1"
|
||||
`shouldRespondWith` [json|[
|
||||
{"name":"Michael Scott", "tasks":[{"name":"Design IOS"}, {"name":"Code IOS"}, {"name":"Design OSX"}]},
|
||||
{"name":"Dwight Schrute","tasks":[{"name":"Design w7"}, {"name":"Design IOS"}]}
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
-- https://github.com/PostgREST/postgrest/issues/2565
|
||||
get "/rpc/get_yards?select=groups(*)"
|
||||
`shouldRespondWith` [json|[]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can embed an M2M relationship table that has a parent relationship table" $
|
||||
get "/rpc/getallusers?select=name,tasks(name,project:projects(name))&id=gt.1"
|
||||
@@ -1247,10 +1273,10 @@ spec actualPgVersion =
|
||||
[json|{"x": 1, "y": 2}|]
|
||||
`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",
|
||||
"hint": "Perhaps you meant to call the function test.unnamed_text_param",
|
||||
"message": "Could not find the function test.unnamed_int_param(x, y) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null
|
||||
"details":"Searched for the function test.unnamed_int_param with parameters x, y or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache."
|
||||
}|]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
@@ -1262,10 +1288,10 @@ spec actualPgVersion =
|
||||
[str|a simple text|]
|
||||
`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",
|
||||
"hint": null,
|
||||
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null
|
||||
"details":"Searched for the function test.unnamed_int_param with a single unnamed text parameter, but no matches were found in the schema cache."
|
||||
}|]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
@@ -1277,10 +1303,10 @@ spec actualPgVersion =
|
||||
[str|a simple text|]
|
||||
`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 xml parameter in the schema cache",
|
||||
"hint": null,
|
||||
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null
|
||||
"details":"Searched for the function test.unnamed_int_param with a single unnamed xml parameter, but no matches were found in the schema cache."
|
||||
}|]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
@@ -1293,10 +1319,10 @@ spec actualPgVersion =
|
||||
file
|
||||
`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",
|
||||
"hint": null,
|
||||
"message": "Could not find the function test.unnamed_int_param in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null
|
||||
"details":"Searched for the function test.unnamed_int_param with a single unnamed bytea parameter, but no matches were found in the schema cache."
|
||||
}|]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
@@ -1353,10 +1379,10 @@ spec actualPgVersion =
|
||||
"a,b\n1,2\n4,6\n100,200"
|
||||
`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",
|
||||
"hint":"Perhaps you meant to call the function test.overloaded_unnamed_param(x, y)",
|
||||
"message":"Could not find the function test.overloaded_unnamed_param(a, b) in the schema cache",
|
||||
"code":"PGRST202",
|
||||
"details":null
|
||||
"details":"Searched for the function test.overloaded_unnamed_param with parameters a, b, but no matches were found in the schema cache."
|
||||
}|]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
|
||||
Vendored
+91
-1
@@ -272,6 +272,56 @@ $_$An RPC function
|
||||
|
||||
Just a test for RPC function arguments$_$;
|
||||
|
||||
CREATE FUNCTION varied_arguments_openapi(
|
||||
double double precision,
|
||||
"varchar" character varying,
|
||||
"boolean" boolean,
|
||||
date date,
|
||||
money money,
|
||||
enum enum_menagerie_type,
|
||||
text_arr text[],
|
||||
int_arr int[],
|
||||
bool_arr boolean[],
|
||||
char_arr char[],
|
||||
varchar_arr varchar[],
|
||||
bigint_arr bigint[],
|
||||
numeric_arr numeric[],
|
||||
json_arr json[],
|
||||
jsonb_arr jsonb[],
|
||||
"integer" integer default 42,
|
||||
json json default '{}',
|
||||
jsonb jsonb default '{}'
|
||||
) RETURNS json
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
AS $_$
|
||||
SELECT json_build_object(
|
||||
'double', double,
|
||||
'varchar', "varchar",
|
||||
'boolean', "boolean",
|
||||
'date', date,
|
||||
'money', money,
|
||||
'enum', enum,
|
||||
'text_arr', text_arr,
|
||||
'int_arr', int_arr,
|
||||
'bool_arr', bool_arr,
|
||||
'char_arr', char_arr,
|
||||
'varchar_arr', varchar_arr,
|
||||
'bigint_arr', bigint_arr,
|
||||
'numeric_arr', numeric_arr,
|
||||
'json_arr', json_arr,
|
||||
'jsonb_arr', jsonb_arr,
|
||||
'integer', "integer",
|
||||
'json', json,
|
||||
'jsonb', jsonb
|
||||
);
|
||||
$_$;
|
||||
|
||||
COMMENT ON FUNCTION varied_arguments_openapi(double precision, character varying, boolean, date, money, enum_menagerie_type, text[], int[], boolean[], char[], varchar[], bigint[], numeric[], json[], jsonb[], integer, json, jsonb) IS
|
||||
$_$An RPC function
|
||||
|
||||
Just a test for RPC function arguments$_$;
|
||||
|
||||
|
||||
CREATE FUNCTION json_argument(arg json) RETURNS text
|
||||
LANGUAGE sql
|
||||
@@ -1814,7 +1864,16 @@ CREATE TABLE test.openapi_types(
|
||||
"a_real" real,
|
||||
"a_double_precision" double precision,
|
||||
"a_json" json,
|
||||
"a_jsonb" jsonb
|
||||
"a_jsonb" jsonb,
|
||||
"a_text_arr" text[],
|
||||
"a_int_arr" int[],
|
||||
"a_bool_arr" boolean[],
|
||||
"a_char_arr" char[],
|
||||
"a_varchar_arr" varchar[],
|
||||
"a_bigint_arr" bigint[],
|
||||
"a_numeric_arr" numeric[],
|
||||
"a_json_arr" json[],
|
||||
"a_jsonb_arr" jsonb[]
|
||||
);
|
||||
|
||||
CREATE TABLE test.openapi_defaults(
|
||||
@@ -2996,3 +3055,34 @@ CREATE TABLE public.tb (
|
||||
|
||||
CREATE VIEW test.va AS SELECT a1 FROM public.ta;
|
||||
CREATE VIEW test.vb AS SELECT b1 FROM public.tb;
|
||||
|
||||
CREATE TABLE test.groups (
|
||||
name text PRIMARY KEY
|
||||
);
|
||||
|
||||
CREATE TABLE test.yards (
|
||||
id bigint PRIMARY KEY
|
||||
);
|
||||
|
||||
CREATE TABLE test.group_yard (
|
||||
id bigint NOT NULL,
|
||||
group_id text NOT NULL REFERENCES test.groups(name),
|
||||
yard_id bigint NOT NULL REFERENCES test.yards(id),
|
||||
PRIMARY KEY (id, group_id, yard_id)
|
||||
);
|
||||
|
||||
CREATE FUNCTION test.get_yards() RETURNS SETOF test.yards
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
select * from test.yards;
|
||||
$$;
|
||||
|
||||
-- view's name is alphabetically before projects
|
||||
create view test.alpha_projects as
|
||||
select c.id, p.name as pro_name, c.name as cli_name
|
||||
from projects p join clients c on p.client_id = c.id;
|
||||
|
||||
-- view's name is alphabetically after projects
|
||||
create view test.zeta_projects as
|
||||
select c.id, p.name as pro_name, c.name as cli_name
|
||||
from projects p join clients c on p.client_id = c.id;
|
||||
Reference in New Issue
Block a user