feat: add related orders
This commit is contained in:
committed by
Steve Chavez
parent
ef42d1c87f
commit
78d45b4e32
@@ -594,10 +594,20 @@ pDelimiter = char '.' <?> "delimiter (.)"
|
||||
--
|
||||
-- >>> P.parse pOrder "" "json_col->key.asc.nullslast"
|
||||
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "clients(json_col->key).desc.nullsfirst"
|
||||
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "clients(name,id)"
|
||||
-- Left (line 1, column 8):
|
||||
-- unexpected '('
|
||||
-- expecting letter, digit, "-", "->>", "->", delimiter (.), "," or end of input
|
||||
--
|
||||
-- >>> P.parse pOrder "" "name,clients(name),id"
|
||||
-- Right [OrderTerm {otTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderRelationTerm {otRelation = "clients", otRelTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderTerm {otTerm = ("id",[]), otDirection = Nothing, otNullOrder = Nothing}]
|
||||
pOrder :: Parser [OrderTerm]
|
||||
pOrder = lexeme pOrderTerm `sepBy1` char ','
|
||||
pOrder = lexeme (try pOrderRelationTerm <|> pOrderTerm) `sepBy1` char ','
|
||||
where
|
||||
pOrderTerm :: Parser OrderTerm
|
||||
pOrderTerm = do
|
||||
fld <- pField
|
||||
dir <- optionMaybe pOrdDir
|
||||
@@ -605,6 +615,13 @@ pOrder = lexeme pOrderTerm `sepBy1` char ','
|
||||
pEnd $> Nothing
|
||||
return $ OrderTerm fld dir nls
|
||||
|
||||
pOrderRelationTerm = do
|
||||
nam <- pFieldName
|
||||
fld <- between (char '(') (char ')') pField
|
||||
dir <- optionMaybe pOrdDir
|
||||
nls <- optionMaybe pNulls <* pEnd <|> pEnd $> Nothing
|
||||
return $ OrderRelationTerm nam fld dir nls
|
||||
|
||||
pNulls :: Parser OrderNulls
|
||||
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
|
||||
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
|
||||
|
||||
@@ -64,6 +64,7 @@ data ApiRequestError
|
||||
| InvalidRpcMethod ByteString
|
||||
| LimitNoOrderError
|
||||
| NotFound
|
||||
| NotToOne Text Text
|
||||
| NoRelBetween Text Text Text
|
||||
| NoRpc Text Text [Text] Bool MediaType Bool
|
||||
| NotEmbedded Text
|
||||
@@ -82,12 +83,19 @@ data RangeError
|
||||
type NodeName = Text
|
||||
type Depth = Integer
|
||||
|
||||
data OrderTerm = OrderTerm
|
||||
{ otTerm :: Field
|
||||
, otDirection :: Maybe OrderDirection
|
||||
, otNullOrder :: Maybe OrderNulls
|
||||
}
|
||||
deriving (Eq)
|
||||
data OrderTerm
|
||||
= OrderTerm
|
||||
{ otTerm :: Field
|
||||
, otDirection :: Maybe OrderDirection
|
||||
, otNullOrder :: Maybe OrderNulls
|
||||
}
|
||||
| OrderRelationTerm
|
||||
{ otRelation :: FieldName
|
||||
, otRelTerm :: Field
|
||||
, otDirection :: Maybe OrderDirection
|
||||
, otNullOrder :: Maybe OrderNulls
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
data OrderDirection
|
||||
= OrderAsc
|
||||
|
||||
+12
-1
@@ -64,6 +64,8 @@ instance PgrstError ApiRequestError where
|
||||
status InvalidRpcMethod{} = HTTP.status405
|
||||
status InvalidRange{} = HTTP.status416
|
||||
status NotFound = HTTP.status404
|
||||
status NotToOne{} = HTTP.status400
|
||||
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status NotEmbedded{} = HTTP.status400
|
||||
@@ -123,7 +125,7 @@ instance JSON.ToJSON ApiRequestError where
|
||||
toJSON NotFound = JSON.object []
|
||||
toJSON (NotEmbedded resource) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode08,
|
||||
"message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text),
|
||||
"message" .= ("'" <> resource <> "' is not an embedded resource in this request" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
|
||||
|
||||
@@ -151,11 +153,18 @@ instance JSON.ToJSON ApiRequestError where
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (NotToOne origin target) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode18,
|
||||
"message" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (NoRelBetween parent child schema) = 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)]
|
||||
|
||||
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),
|
||||
@@ -461,6 +470,7 @@ data ErrorCode
|
||||
| ApiRequestErrorCode15
|
||||
| ApiRequestErrorCode16
|
||||
| ApiRequestErrorCode17
|
||||
| ApiRequestErrorCode18
|
||||
-- Schema Cache errors
|
||||
| SchemaCacheErrorCode00
|
||||
| SchemaCacheErrorCode01
|
||||
@@ -502,6 +512,7 @@ buildErrorCode code = "PGRST" <> case code of
|
||||
ApiRequestErrorCode15 -> "115"
|
||||
ApiRequestErrorCode16 -> "116"
|
||||
ApiRequestErrorCode17 -> "117"
|
||||
ApiRequestErrorCode18 -> "118"
|
||||
|
||||
SchemaCacheErrorCode00 -> "200"
|
||||
SchemaCacheErrorCode01 -> "201"
|
||||
|
||||
+25
-1
@@ -52,7 +52,8 @@ import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..),
|
||||
RelationshipsMap)
|
||||
RelationshipsMap,
|
||||
relIsToOne)
|
||||
import PostgREST.SchemaCache.Table (tablePKCols)
|
||||
|
||||
import PostgREST.Plan.CallPlan
|
||||
@@ -96,6 +97,7 @@ readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Eit
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbRelationships} apiRequest =
|
||||
mapLeft ApiRequestError $
|
||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||
addRelatedOrders =<<
|
||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||
addLogicTrees apiRequest =<<
|
||||
addRanges apiRequest =<<
|
||||
@@ -299,6 +301,28 @@ addOrders ApiRequest{..} rReq =
|
||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=o} f)
|
||||
|
||||
-- Validates that the related resource on the order is an embedded resource,
|
||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
||||
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
||||
-- TODO might be clearer if there's an additional intermediate type
|
||||
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
newOrder <- getRelOrder `traverse` order
|
||||
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
||||
where
|
||||
getRelOrder ot@OrderTerm{} = Right ot
|
||||
getRelOrder ot@OrderRelationTerm{otRelation} =
|
||||
let foundRP = rootLabel <$> find (\(Node ReadPlan{relName, relAlias} _) -> Just otRelation `elem` [Just relName, relAlias] ) forest in
|
||||
case foundRP of
|
||||
Just ReadPlan{relName,relAlias,relAggAlias,relToParent} ->
|
||||
let isToOne = relIsToOne <$> relToParent
|
||||
name = fromMaybe relName relAlias in
|
||||
if isToOne == Just True
|
||||
then Right $ ot{otRelation=relAggAlias}
|
||||
else Left $ NotToOne (qiName from) name
|
||||
Nothing ->
|
||||
Left $ NotEmbedded otRelation
|
||||
|
||||
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRanges ApiRequest{..} rReq =
|
||||
case iAction of
|
||||
|
||||
@@ -27,7 +27,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Proc (ProcParam (..))
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..))
|
||||
Relationship (..),
|
||||
relIsToOne)
|
||||
|
||||
import PostgREST.ApiRequest.Types
|
||||
import PostgREST.Plan.CallPlan
|
||||
@@ -63,12 +64,7 @@ getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, re
|
||||
aggAlias = pgFmtIdent relAggAlias
|
||||
correlatedSubquery sub al cond =
|
||||
(if joinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> SQL.sql al <> " ON " <> cond
|
||||
isToOne = case rel of
|
||||
Relationship{relCardinality=M2O _ _} -> True
|
||||
Relationship{relCardinality=O2O _ _} -> True
|
||||
ComputedRelationship{relToOne=True} -> True
|
||||
_ -> False
|
||||
(sel, joi) = if isToOne
|
||||
(sel, joi) = if relIsToOne rel
|
||||
then
|
||||
( SQL.sql ("row_to_json(" <> aggAlias <> ".*) AS " <> aliasOrName)
|
||||
, correlatedSubquery subquery aggAlias "TRUE")
|
||||
|
||||
@@ -244,11 +244,15 @@ pgFmtSelectItem table (f@(fName, jp), Just cast, alias) = "CAST (" <> pgFmtField
|
||||
|
||||
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SQL.Snippet
|
||||
pgFmtOrderTerm qi ot =
|
||||
pgFmtField qi (otTerm ot) <> " " <>
|
||||
fmtOTerm ot <> " " <>
|
||||
SQL.sql (BS.unwords [
|
||||
maybe mempty direction $ otDirection ot,
|
||||
maybe mempty nullOrder $ otNullOrder ot])
|
||||
where
|
||||
fmtOTerm = \case
|
||||
OrderTerm{otTerm} -> pgFmtField qi otTerm
|
||||
OrderRelationTerm{otRelation, otRelTerm} -> pgFmtField (QualifiedIdentifier mempty otRelation) otRelTerm
|
||||
|
||||
direction OrderAsc = "ASC"
|
||||
direction OrderDesc = "DESC"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ module PostgREST.SchemaCache.Relationship
|
||||
, Relationship(..)
|
||||
, Junction(..)
|
||||
, RelationshipsMap
|
||||
, relIsToOne
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -62,3 +63,10 @@ data Junction = Junction
|
||||
|
||||
-- | Key based on the source table and the foreign table schema
|
||||
type RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema) [Relationship]
|
||||
|
||||
relIsToOne :: Relationship -> Bool
|
||||
relIsToOne rel = case rel of
|
||||
Relationship{relCardinality=M2O _ _} -> True
|
||||
Relationship{relCardinality=O2O _ _} -> True
|
||||
ComputedRelationship{relToOne=True} -> True
|
||||
_ -> False
|
||||
|
||||
Reference in New Issue
Block a user