add: config to emit warning for legacy target names
Adds the `url_use_legacy_target_names` config. Enabled (default): * It allows using the resource name in filters, orders or limits when it has an alias, e.g. `table?select=alias:target(*)&target.id=eq.1` * Logs a WARNING with a hint to use the alias * Returns a Warning header in the response Disabled: * It returns an error, only the alias is allowed * No warnings returned This feature is deprecated
This commit is contained in:
committed by
Laurence Isla
parent
2fa8de4e52
commit
490d1dc5d3
+8
-2
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
- Shutdown should wait for in flight requests by @mkleczek in #4702
|
||||
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
|
||||
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
|
||||
+ You need to set `url-use-legacy-target-names = false`.
|
||||
- If the schema cache fails to reload, PostgREST will no longer stop serving requests and will continue doing so in a "best effort" basis by @mkleczek in #4873 #4869
|
||||
- Stop reporting 503s errors unnecessarily while the schema cache is loading at startup by @mkleczek in #4880
|
||||
- Fix responding with `Something went wrong` on Admin server when under EMFILE by @mkleczek in #5077
|
||||
@@ -34,8 +35,6 @@ All notable changes to this project will be documented in this file. From versio
|
||||
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
|
||||
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
|
||||
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
|
||||
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
|
||||
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
|
||||
- Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984
|
||||
|
||||
#### Changed Syntax for JWT Role Extraction
|
||||
@@ -50,6 +49,13 @@ The `jwt-role-claim-key` config should be updated according to the following:
|
||||
+ Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]`
|
||||
- Detailed reference for syntax: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate filters, orders and limits with the name of an embedded table when it has an alias by @steve-chavez, @laurenceisla in #4075
|
||||
+ e.g. `?select=alias:table(*)&table.id=eq.1` will not be possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
|
||||
+ You will see a warning in the logs when this happens.
|
||||
+ You can disable this behavior now by setting `url-use-legacy-target-names = false`.
|
||||
|
||||
## [14.14] - 2026-06-29
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1013,3 +1013,37 @@ server-unix-socket-mode
|
||||
.. code:: bash
|
||||
|
||||
server-unix-socket-mode = "660"
|
||||
|
||||
.. _url-use-legacy-target-names:
|
||||
|
||||
url-use-legacy-target-names
|
||||
---------------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_URL_USE_LEGACY_TARGET_NAMES
|
||||
**In-Database** pgrst.url_use_legacy_target_names
|
||||
=============== =================================
|
||||
|
||||
When active, it allows using the the name of an embedded table in filters, orders or limits even if it has an alias:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl "http://localhost:3000/table?select=alias:target(*)&target.order=id" -i
|
||||
|
||||
.. code:: text
|
||||
|
||||
Warning: 299 PostgRESTv16 "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release. Update `target` to `alias` in query string filters, orders or limits."
|
||||
[...]
|
||||
|
||||
Note that the response includes a deprecation message in the ``Warning`` header.
|
||||
This will also show in the PostgREST logs:
|
||||
|
||||
.. code::
|
||||
|
||||
28/May/2026:20:33:22 -0500: WARNING: Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release.
|
||||
28/May/2026:20:33:22 -0500: Update filters, orders or limits that use `target` to `alias` in `GET /table?select=alias:target(*)&target.order=id`
|
||||
|
||||
This feature will be removed in a future release, so you should start using the ``alias`` in these cases.
|
||||
|
||||
+23
-5
@@ -71,7 +71,7 @@ import qualified Data.List as L
|
||||
import Data.Streaming.Network (bindPortTCP)
|
||||
import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import Network.HTTP.Types.Header (hVary)
|
||||
import Network.HTTP.Types.Header (hVary, hWarning)
|
||||
import qualified Network.Socket as NS
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
import System.Posix.Types (FileMode)
|
||||
@@ -210,6 +210,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
||||
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||
|
||||
let warnings = Plan.legacyWarnings plan
|
||||
legacyWarnMsg = "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
|
||||
legacyWarnHint = let replacement (relName, alias) = "`" <> relName <> "` to `" <> alias <> "`" in T.intercalate ", " (replacement <$> warnings)
|
||||
shouldShowWarnings = configUrlUseLegacyTargetNames && not (null warnings)
|
||||
|
||||
liftIO $ when shouldShowWarnings $
|
||||
observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency
|
||||
|
||||
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
|
||||
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
|
||||
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
|
||||
@@ -235,12 +243,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
||||
liftIO $ obsQuery status'
|
||||
liftEither response
|
||||
|
||||
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
|
||||
let warnHdrMsgs = if shouldShowWarnings then Just (legacyWarnMsg, legacyWarnHint) else Nothing
|
||||
|
||||
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) warnHdrMsgs resp
|
||||
|
||||
where
|
||||
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
|
||||
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
|
||||
toWaiResponse :: ServerTiming -> Maybe (Text, Text) -> Response.PgrstResponse -> Wai.Response
|
||||
toWaiResponse timing warnMsgs (Response.PgrstResponse st hdrs bod) =
|
||||
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ warningHeaders warnMsgs ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
|
||||
|
||||
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
|
||||
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
|
||||
@@ -251,6 +261,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
||||
varyHeaderPresent :: [HTTP.Header] -> Bool
|
||||
varyHeaderPresent = any (\(h, _v) -> h == hVary)
|
||||
|
||||
warningHeaders :: Maybe (Text, Text) -> [HTTP.Header]
|
||||
warningHeaders Nothing = []
|
||||
warningHeaders (Just (msg, hint)) =
|
||||
let warnMsg = msg <> " Update " <> hint <> " in query string filters, orders or limits."
|
||||
pgrstVer = "PostgRESTv" <> BS.filter (/= ' ') prettyVersion
|
||||
in
|
||||
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
|
||||
|
||||
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
|
||||
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
|
||||
then do
|
||||
|
||||
@@ -121,6 +121,7 @@ data AppConfig = AppConfig
|
||||
, configServerTimingEnabled :: Bool
|
||||
, configServerUnixSocket :: Maybe FilePath
|
||||
, configServerUnixSocketMode :: FileMode
|
||||
, configUrlUseLegacyTargetNames :: Bool
|
||||
, configAdminServerHost :: Text
|
||||
, configAdminServerPort :: Maybe Int
|
||||
, configAdminServerUnixSocket :: Maybe FilePath
|
||||
@@ -207,6 +208,7 @@ toText conf =
|
||||
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
|
||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||
,("url-use-legacy-target-names", T.toLower . show . configUrlUseLegacyTargetNames)
|
||||
,("admin-server-host", q . configAdminServerHost)
|
||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||
,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket)
|
||||
@@ -325,6 +327,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe False <$> optBool "server-timing-enabled")
|
||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||
<*> (fromMaybe True <$> optBool "url-use-legacy-target-names")
|
||||
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
|
||||
(optString "server-host"))
|
||||
<*> parseAdminServerPort "admin-server-port"
|
||||
@@ -794,4 +797,9 @@ exampleConfigFile = S.unlines
|
||||
, "## Unix socket file mode"
|
||||
, "## When none is provided, 660 is applied by default"
|
||||
, "# server-unix-socket-mode = \"660\""
|
||||
, ""
|
||||
, "## Use legacy target names in relationship filters"
|
||||
, "## If active, allows using the target name of the relationship in filters even if it has an alias."
|
||||
, "## Otherwise it only allows the alias in filters"
|
||||
, "url-use-legacy-target-names = true"
|
||||
]
|
||||
|
||||
@@ -71,6 +71,7 @@ dbSettingsNames =
|
||||
,"server_cors_allowed_origins"
|
||||
,"server_trace_header"
|
||||
,"server_timing_enabled"
|
||||
,"url_use_legacy_target_names"
|
||||
]
|
||||
|
||||
queryPgVersion :: Session PgVersion
|
||||
|
||||
@@ -175,7 +175,7 @@ instance ErrorBody ApiRequestError where
|
||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
||||
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||
message (NotEmbedded resource _) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
||||
message GucStatusError = "response.status guc must be a valid status code"
|
||||
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
|
||||
@@ -207,11 +207,13 @@ instance ErrorBody ApiRequestError where
|
||||
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
|
||||
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
|
||||
details (NotImplemented details') = Just $ JSON.String details'
|
||||
details (NotEmbedded _ (Just _)) = Just $ JSON.String "Target names are not allowed in filters if they have an alias"
|
||||
|
||||
details _ = Nothing
|
||||
|
||||
-- HINT: Maybe JSON.Value
|
||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (NotEmbedded resource Nothing) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (NotEmbedded _ (Just (name, alias))) = Just $ JSON.String $ "Change '" <> name <> "' to '" <> alias <> "' in filters, orders or limits."
|
||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ data ApiRequestError
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| NotEmbedded Text
|
||||
| NotEmbedded Text (Maybe (Text, Text))
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
|
||||
@@ -192,6 +192,10 @@ observationMessages = \case
|
||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||
in
|
||||
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
|
||||
LegacyTargetNameWarningObs (warningMsg, warningHints) requestMethod requestTarget ->
|
||||
[ "WARNING: " <> warningMsg
|
||||
, "Update filters, orders or limits that use " <> warningHints <> " in " <> "`" <> T.decodeUtf8 (requestMethod <> " " <> requestTarget) <> "`"
|
||||
]
|
||||
ConfigReadErrorObs usageErr ->
|
||||
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||
QueryRoleSettingsErrorObs usageErr ->
|
||||
|
||||
@@ -45,6 +45,7 @@ data Observation
|
||||
| DBListenerGotConfigMsg ByteString
|
||||
| DBListenerConnectionCleanupFail SomeException
|
||||
| QueryObs MainQuery Status
|
||||
| LegacyTargetNameWarningObs (Text, Text) ByteString ByteString
|
||||
| ConfigReadErrorObs SQL.UsageError
|
||||
| ConfigInvalidObs Text
|
||||
| ConfigSucceededObs
|
||||
|
||||
+69
-30
@@ -23,6 +23,7 @@ module PostgREST.Plan
|
||||
, InspectPlan(..)
|
||||
, InfoPlan(..)
|
||||
, CrudPlan(..)
|
||||
, legacyWarnings
|
||||
) where
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
@@ -144,6 +145,25 @@ data InfoPlan
|
||||
| RoutineInfoPlan Routine -- info about function
|
||||
| SchemaInfoPlan -- info about schema cache
|
||||
|
||||
legacyWarnings :: ActionPlan -> [(Text, Text)]
|
||||
legacyWarnings (NoDb _) = []
|
||||
legacyWarnings (Db dbPlan) =
|
||||
case dbPlan of
|
||||
DbCrud _ crudPlan ->
|
||||
readPlanWarnings $ case crudPlan of
|
||||
WrappedReadPlan{wrReadPlan} -> wrReadPlan
|
||||
MutateReadPlan{mrReadPlan} -> mrReadPlan
|
||||
CallReadPlan{crReadPlan} -> crReadPlan
|
||||
MayUseDb _ -> []
|
||||
where
|
||||
readPlanWarnings :: ReadPlanTree -> [(Text, Text)]
|
||||
readPlanWarnings (Node rp forest) =
|
||||
maybeToList (readPlanWarning rp) <> foldMap readPlanWarnings forest
|
||||
|
||||
readPlanWarning :: ReadPlan -> Maybe (Text, Text)
|
||||
readPlanWarning ReadPlan{relName, relAlias = Just alias, relIsLegacyTargetNameMatch = True} = Just (relName, alias)
|
||||
readPlanWarning _ = Nothing
|
||||
|
||||
actionPlan :: Action -> AppConfig -> ApiRequest -> SchemaCache -> Either Error ActionPlan
|
||||
actionPlan act conf apiReq sCache = case act of
|
||||
ActDb dbAct -> Db <$> dbActionPlan dbAct conf apiReq sCache
|
||||
@@ -354,7 +374,7 @@ resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnkno
|
||||
-- | Adds filters, order, limits on its respective nodes.
|
||||
-- | Adds joins conditions obtained from resource embedding.
|
||||
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates, configUrlUseLegacyTargetNames} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||
let
|
||||
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
@@ -369,10 +389,10 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
|
||||
addAliases =<<
|
||||
expandStars ctx =<<
|
||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||
addLogicTrees ctx apiRequest =<<
|
||||
addRanges apiRequest =<<
|
||||
addOrders ctx apiRequest =<<
|
||||
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
||||
addLogicTrees ctx apiRequest configUrlUseLegacyTargetNames =<<
|
||||
addRanges apiRequest configUrlUseLegacyTargetNames =<<
|
||||
addOrders ctx apiRequest configUrlUseLegacyTargetNames =<<
|
||||
addFilters ctx apiRequest configUrlUseLegacyTargetNames (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
||||
|
||||
-- Build the initial read plan tree
|
||||
initReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree
|
||||
@@ -380,7 +400,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
||||
where
|
||||
rootDepth = 0
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth False
|
||||
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
||||
treeEntry depth (Node si fldForest) (Node q rForest) =
|
||||
let nxtDepth = succ depth in
|
||||
@@ -816,8 +836,8 @@ findTable qi@QualifiedIdentifier{..} sc@SchemaCache{dbTables} =
|
||||
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName sc
|
||||
Just _ -> Right qi
|
||||
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addFilters ctx ApiRequest{..} rReq =
|
||||
addFilters :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addFilters ctx ApiRequest{..} useTargetNames rReq =
|
||||
foldr addFilterToNode (Right rReq) flts
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
@@ -829,15 +849,15 @@ addFilters ctx ApiRequest{..} rReq =
|
||||
|
||||
addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addFilterToNode =
|
||||
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||
updateNode useTargetNames (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||
|
||||
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder
|
||||
addOrders :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addOrders ctx ApiRequest{..} useTargetNames rReq = foldr addOrderToNode (Right rReq) qsOrder
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
|
||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
||||
addOrderToNode = updateNode useTargetNames (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
||||
|
||||
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
||||
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
||||
@@ -862,7 +882,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
then Right $ cot{coRelation=relAggAlias}
|
||||
else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name
|
||||
Nothing ->
|
||||
Left $ ApiRequestErr $ NotEmbedded coRelation
|
||||
Left $ ApiRequestErr $ NotEmbedded coRelation Nothing
|
||||
|
||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||
--
|
||||
@@ -887,7 +907,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- relToParent = Nothing,
|
||||
-- relJoinConds = [],
|
||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1,
|
||||
-- relSelect = []
|
||||
-- relSelect = [],
|
||||
-- relIsLegacyTargetNameMatch = False
|
||||
-- },
|
||||
-- subForest = []
|
||||
-- }
|
||||
@@ -913,7 +934,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- ],
|
||||
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
|
||||
-- relJoinType = Nothing, relSpread = Nothing, depth = 0,
|
||||
-- relSelect = []
|
||||
-- relSelect = [],
|
||||
-- relIsLegacyTargetNameMatch = False
|
||||
-- },
|
||||
-- subForest = subForst
|
||||
-- }
|
||||
@@ -949,8 +971,8 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
||||
flt@(CoercibleStmnt _) ->
|
||||
Right flt
|
||||
|
||||
addRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRanges ApiRequest{..} rReq =
|
||||
addRanges :: ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRanges ApiRequest{..} useTargetNames rReq =
|
||||
case iAction of
|
||||
ActDb (ActRelationMut _ _) -> Right rReq
|
||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||
@@ -959,10 +981,10 @@ addRanges ApiRequest{..} rReq =
|
||||
ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||
addRangeToNode = updateNode useTargetNames (\r (Node q f) -> Node q{range_=r} f)
|
||||
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTrees ctx ApiRequest{..} rReq =
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTrees ctx ApiRequest{..} useTargetNames rReq =
|
||||
foldr addLogicTreeToNode (Right rReq) logic
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
@@ -975,7 +997,7 @@ addLogicTrees ctx ApiRequest{..} rReq =
|
||||
_ -> filter (not . null . fst) qsLogic
|
||||
|
||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||
addLogicTreeToNode = updateNode useTargetNames (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||
|
||||
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
|
||||
resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
|
||||
@@ -985,18 +1007,35 @@ resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
|
||||
|
||||
-- Find a Node of the Tree and apply a function to it
|
||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
updateNode f ([], a) rr = f a <$> rr
|
||||
updateNode _ _ (Left e) = Left e
|
||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
case findNode of
|
||||
Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName
|
||||
updateNode :: Bool -> (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
updateNode _ f ([], a) rr = f a <$> rr
|
||||
updateNode _ _ _ (Left e) = Left e
|
||||
updateNode useTargetNames f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
case findNode useTargetNames of
|
||||
Nothing ->
|
||||
Left $ ApiRequestErr $ NotEmbedded targetNodeName findLegacyUsage
|
||||
Just target ->
|
||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||
updateNode f (remainingPath, a) (Right target)
|
||||
updateNode useTargetNames f (remainingPath, a) (Right $ updateLegacyAttrs target)
|
||||
where
|
||||
findNode :: Maybe ReadPlanTree
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> fromMaybe relName relAlias == targetNodeName) forest
|
||||
findNode :: Bool -> Maybe ReadPlanTree
|
||||
findNode isLegacy = find (matchTarget isLegacy) forest
|
||||
|
||||
matchTarget :: Bool -> ReadPlanTree -> Bool
|
||||
matchTarget isLegacy (Node ReadPlan{relName, relAlias} _)
|
||||
| isLegacy = relName == targetNodeName || relAlias == Just targetNodeName
|
||||
| otherwise = fromMaybe relName relAlias == targetNodeName
|
||||
|
||||
updateLegacyAttrs :: ReadPlanTree -> ReadPlanTree
|
||||
updateLegacyAttrs node@(Node rPlan@ReadPlan{relName, relAlias} children)
|
||||
| relName == targetNodeName && isJust relAlias && relAlias /= Just targetNodeName =
|
||||
Node rPlan{relIsLegacyTargetNameMatch=True} children
|
||||
| otherwise = node
|
||||
|
||||
findLegacyUsage :: Maybe (Text, Text)
|
||||
findLegacyUsage
|
||||
| useTargetNames = Nothing
|
||||
| otherwise = (\(Node rp _) -> fromMaybe mempty $ readPlanWarning rp) . updateLegacyAttrs <$> findNode True
|
||||
|
||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
||||
|
||||
@@ -32,22 +32,22 @@ data JoinCondition =
|
||||
|
||||
-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable
|
||||
data ReadPlan = ReadPlan
|
||||
{ select :: [CoercibleSelectField]
|
||||
, from :: QualifiedIdentifier
|
||||
, fromAlias :: Maybe Alias
|
||||
, where_ :: [CoercibleLogicTree]
|
||||
, order :: [CoercibleOrderTerm]
|
||||
, range_ :: NonnegRange
|
||||
, relName :: NodeName
|
||||
, relToParent :: Maybe Relationship
|
||||
, relJoinConds :: [JoinCondition]
|
||||
, relAlias :: Maybe Alias
|
||||
, relAggAlias :: Alias
|
||||
, relHint :: Maybe Hint
|
||||
, relJoinType :: Maybe JoinType
|
||||
, relSpread :: Maybe SpreadType
|
||||
, relSelect :: [RelSelectField]
|
||||
, depth :: Depth
|
||||
-- ^ used for aliasing
|
||||
{ select :: [CoercibleSelectField]
|
||||
, from :: QualifiedIdentifier
|
||||
, fromAlias :: Maybe Alias
|
||||
, where_ :: [CoercibleLogicTree]
|
||||
, order :: [CoercibleOrderTerm]
|
||||
, range_ :: NonnegRange
|
||||
, relName :: NodeName
|
||||
, relToParent :: Maybe Relationship
|
||||
, relJoinConds :: [JoinCondition]
|
||||
, relAlias :: Maybe Alias
|
||||
, relAggAlias :: Alias
|
||||
, relHint :: Maybe Hint
|
||||
, relJoinType :: Maybe JoinType
|
||||
, relSpread :: Maybe SpreadType
|
||||
, relSelect :: [RelSelectField]
|
||||
, depth :: Depth -- ^ used for aliasing
|
||||
, relIsLegacyTargetNameMatch :: Bool -- ^ used to ease migration into a new version with breaking change
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1,7 +1,59 @@
|
||||
- - - qiName: awards
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - director_id
|
||||
- id
|
||||
relCons: awards_director_id_fkey
|
||||
tag: M2O
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: directors
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - film_id
|
||||
- id
|
||||
relCons: awards_film_id_fkey
|
||||
tag: M2O
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: films
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
|
||||
- - - qiName: directors
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- director_id
|
||||
relCons: awards_director_id_fkey
|
||||
tag: O2M
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: directors
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- director_id
|
||||
@@ -22,6 +74,22 @@
|
||||
qiSchema: public
|
||||
- public
|
||||
- - relCardinality:
|
||||
relColumns:
|
||||
- - id
|
||||
- film_id
|
||||
relCons: awards_film_id_fkey
|
||||
tag: O2M
|
||||
relFTableIsView: false
|
||||
relForeignTable:
|
||||
qiName: awards
|
||||
qiSchema: public
|
||||
relIsSelf: false
|
||||
relTable:
|
||||
qiName: films
|
||||
qiSchema: public
|
||||
relTableIsView: false
|
||||
tag: Relationship
|
||||
- relCardinality:
|
||||
relColumns:
|
||||
- - director_id
|
||||
- id
|
||||
|
||||
@@ -126,6 +126,64 @@
|
||||
tableSchema: public
|
||||
tableUpdatable: false
|
||||
|
||||
- - qiName: awards
|
||||
qiSchema: public
|
||||
- tableColumns:
|
||||
director_id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: director_id
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
film_id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: film_id
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
id:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: id
|
||||
colNominalType: integer
|
||||
colNullable: false
|
||||
colType: integer
|
||||
name:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: name
|
||||
colNominalType: text
|
||||
colNullable: true
|
||||
colType: text
|
||||
year:
|
||||
colDefault: null
|
||||
colDescription: null
|
||||
colEnum: []
|
||||
colMaxLen: null
|
||||
colName: year
|
||||
colNominalType: integer
|
||||
colNullable: true
|
||||
colType: integer
|
||||
tableDeletable: true
|
||||
tableDescription: null
|
||||
tableInsertable: true
|
||||
tableIsView: false
|
||||
tableName: awards
|
||||
tablePKCols:
|
||||
- id
|
||||
tableSchema: public
|
||||
tableUpdatable: true
|
||||
|
||||
- - qiName: films
|
||||
qiSchema: public
|
||||
- tableColumns:
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = true
|
||||
server-trace-header = "traceparent"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = false
|
||||
server-trace-header = "CF-Ray"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -44,3 +44,4 @@ server-timing-enabled = true
|
||||
server-trace-header = "X-Request-Id"
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = false
|
||||
|
||||
@@ -43,3 +43,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -42,3 +42,4 @@ server-timing-enabled = false
|
||||
server-trace-header = ""
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
url-use-legacy-target-names = true
|
||||
|
||||
@@ -41,6 +41,7 @@ PGRST_SERVER_TRACE_HEADER: X-Request-Id
|
||||
PGRST_SERVER_TIMING_ENABLED: true
|
||||
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
||||
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
||||
PGRST_URL_USE_LEGACY_TARGET_NAMES: false
|
||||
PGRST_ADMIN_SERVER_HOST: 127.0.0.1
|
||||
PGRST_ADMIN_SERVER_PORT: 3001
|
||||
PGRST_ADMIN_SERVER_UNIX_SOCKET: /tmp/admin_io_test.sock
|
||||
|
||||
@@ -38,6 +38,7 @@ server-trace-header = "X-Request-Id"
|
||||
server-timing-enabled = true
|
||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||
server-unix-socket-mode = "777"
|
||||
url-use-legacy-target-names = false
|
||||
admin-server-port = 3001
|
||||
admin-server-host = "127.0.0.1"
|
||||
admin-server-unix-socket = "/tmp/admin_io_test.sock"
|
||||
|
||||
@@ -25,6 +25,7 @@ ALTER ROLE db_config_authenticator SET pgrst.openapi_server_proxy_uri = 'https:/
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://origin.com';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_timing_enabled = 'false';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.url_use_legacy_target_names = 'true';
|
||||
ALTER ROLE db_config_authenticator SET pgrst.db_hoisted_tx_settings = 'autovacuum_work_mem';
|
||||
|
||||
-- override with database specific setting
|
||||
@@ -81,6 +82,7 @@ ALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://oth
|
||||
ALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://otherorigin.com';
|
||||
ALTER ROLE other_authenticator SET pgrst.server_timing_enabled = 'true';
|
||||
ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';
|
||||
ALTER ROLE other_authenticator SET pgrst.url_use_legacy_target_names = 'true';
|
||||
ALTER ROLE other_authenticator SET pgrst.db_hoisted_tx_settings = 'maintenance_work_mem';
|
||||
|
||||
create schema postgrest;
|
||||
|
||||
@@ -3,7 +3,7 @@ GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
|
||||
|
||||
GRANT SELECT ON authors_only TO postgrest_test_author;
|
||||
GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
GRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
GRANT SELECT ON directors, films, awards TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
|
||||
|
||||
GRANT ALL ON cats TO postgrest_test_anonymous;
|
||||
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
|
||||
|
||||
@@ -25,6 +25,14 @@ CREATE TABLE films (
|
||||
on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE awards (
|
||||
id int primary key,
|
||||
name text,
|
||||
year int,
|
||||
film_id int references films(id),
|
||||
director_id int references directors(id)
|
||||
);
|
||||
|
||||
-- data to test resource embedding
|
||||
TRUNCATE TABLE directors CASCADE;
|
||||
INSERT INTO directors
|
||||
|
||||
@@ -2089,3 +2089,33 @@ def test_work_mem_in_role_settings(defaultenv):
|
||||
response = postgrest.session.post("/rpc/get_work_mem", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == '"3MB"'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", ["true", "false"])
|
||||
def test_use_legacy_target_names(enabled, defaultenv):
|
||||
"Show a warning when a target name is used instead of an alias, only when config is enabled"
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_URL_USE_LEGACY_TARGET_NAMES": enabled,
|
||||
}
|
||||
|
||||
with run(env=env) as postgrest:
|
||||
response = postgrest.session.get(
|
||||
"/directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026"
|
||||
)
|
||||
|
||||
output = postgrest.read_stdout(nlines=10)
|
||||
|
||||
log_err_warning = "WARNING: Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
|
||||
log_err_hint = "Update filters, orders or limits that use `films` to `all_films`, `awards` to `awards_2026` in `GET /directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026`"
|
||||
|
||||
has_warning_log = any(log_err_warning in line for line in output)
|
||||
has_hint_log = any(log_err_hint in line for line in output)
|
||||
|
||||
if enabled == "true":
|
||||
assert response.status_code == 200
|
||||
assert has_warning_log and has_hint_log
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
assert not has_warning_log and not has_hint_log
|
||||
|
||||
@@ -33,7 +33,6 @@ CREATE TABLE test.roles (
|
||||
character TEXT
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE test.authors_only ();
|
||||
|
||||
CREATE FUNCTION test.call_me (name TEXT) RETURNS TEXT
|
||||
|
||||
@@ -122,6 +122,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configInternalSCQuerySleepFst = Nothing
|
||||
, configInternalSCQuerySleepSnd = Nothing
|
||||
, configServerTimingEnabled = True
|
||||
, configUrlUseLegacyTargetNames = True
|
||||
}
|
||||
|
||||
testCfg :: AppConfig
|
||||
|
||||
@@ -7,6 +7,7 @@ import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion190)
|
||||
|
||||
import Protolude hiding (get)
|
||||
@@ -1071,16 +1072,6 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
{ "id":4,"children":[]}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when embedding the same table more than once" $
|
||||
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name":"Lake",
|
||||
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
|
||||
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
|
||||
}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "ordering response" $ do
|
||||
it "by a column asc" $
|
||||
get "/items?id=lte.2&order=id.asc"
|
||||
@@ -1186,6 +1177,12 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
]
|
||||
}
|
||||
|
||||
it "filters, orders and limits aliased embeded entities using the target name" $
|
||||
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = ["Warning" <:> "299 PostgRESTv15(pre-release) \"Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release. Update `tasks` to `the_tasks` in query string filters, orders or limits.\""] }
|
||||
|
||||
|
||||
describe "Accept headers" $ do
|
||||
it "should respond an unknown accept type with 406" $
|
||||
request methodGet "/simple_pk"
|
||||
@@ -1690,3 +1687,30 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
||||
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = ["Content-Length" <:> "96"]}
|
||||
|
||||
specLegacyTargetNames :: SpecWithConfig
|
||||
specLegacyTargetNames withConfig = withConfig (baseCfg { configUrlUseLegacyTargetNames = False }) $
|
||||
context "disable legacy target names" $ do
|
||||
describe "Shaping response with select parameter" $ do
|
||||
it "works when embedding the same table more than once" $
|
||||
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
|
||||
[json|[
|
||||
{
|
||||
"name":"Lake",
|
||||
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
|
||||
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
|
||||
}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
describe "ordering response" $
|
||||
it "filters, orders or limits do not work with aliased embeded entities using the target name" $
|
||||
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"code":"PGRST108",
|
||||
"details":"Target names are not allowed in filters if they have an alias",
|
||||
"hint":"Change 'tasks' to 'the_tasks' in filters, orders or limits.",
|
||||
"message":"'tasks' is not an embedded resource in this request"
|
||||
}
|
||||
|]
|
||||
{ matchStatus = 400,
|
||||
matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -152,6 +152,7 @@ main = do
|
||||
, ("Feature.Query.PreparedStatementsSpec.spec" , Feature.Query.PreparedStatementsSpec.spec)
|
||||
, ("Feature.Query.QueryLimitedSpec" , Feature.Query.QueryLimitedSpec.spec)
|
||||
, ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion)
|
||||
, ("Feature.Query.QuerySpec.specLegacyTargetNames" , Feature.Query.QuerySpec.specLegacyTargetNames)
|
||||
, ("Feature.Query.RangeSpec" , Feature.Query.RangeSpec.spec)
|
||||
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
|
||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
||||
|
||||
@@ -175,6 +175,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configServerTraceHeader = Nothing
|
||||
, configServerUnixSocket = Nothing
|
||||
, configServerUnixSocketMode = 432
|
||||
, configUrlUseLegacyTargetNames = True
|
||||
, configDbTxAllowOverride = True
|
||||
, configDbTxRollbackAll = True
|
||||
, configAdminServerHost = "localhost"
|
||||
|
||||
Reference in New Issue
Block a user