Allow specifying the constraint name to disambiguate an embedding (#1430)
Makes previous duck typing regex unnecessary since the FK can be renamed to a singular name or to any other format. * Remove embedding with duck typed column names * Allow embedding by foreign key name * Add junction disambiguation tests
This commit is contained in:
@@ -16,18 +16,14 @@ module PostgREST.DbRequestBuilder (
|
||||
, mutateRequest
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Foldable (foldr1)
|
||||
import Data.List (delete, head, (!!))
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.List (delete)
|
||||
import Data.Text (isInfixOf)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Unsafe (unsafeHead)
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Tree
|
||||
@@ -38,7 +34,7 @@ import PostgREST.Error (ApiRequestError (..), errorResponseFor)
|
||||
import PostgREST.Parsers
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (from, head)
|
||||
import Protolude hiding (from)
|
||||
|
||||
readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest
|
||||
readRequest schema rootTableName maxRows allRels apiRequest =
|
||||
@@ -49,20 +45,20 @@ readRequest schema rootTableName maxRows allRels apiRequest =
|
||||
(initReadRequest rootName <$> pRequestSelect sel)
|
||||
where
|
||||
sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param
|
||||
(rootName, rootRels) = rootWithRelations schema rootTableName allRels (iAction apiRequest)
|
||||
(rootName, rootRels) = rootWithRels schema rootTableName allRels (iAction apiRequest)
|
||||
|
||||
-- Get the root table name with its relations according to the Action type.
|
||||
-- Get the root table name with its relationships according to the Action type.
|
||||
-- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs).
|
||||
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
|
||||
rootWithRelations :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
||||
rootWithRelations schema rootTableName allRels action = case action of
|
||||
rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
||||
rootWithRels schema rootTableName allRels action = case action of
|
||||
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
|
||||
_ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRelation allRels ++ allRels) -- mutation cases and calling proc
|
||||
_ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRel allRels ++ allRels) -- mutation cases and calling proc
|
||||
where
|
||||
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
|
||||
-- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName.
|
||||
toSourceRelation :: Relation -> Maybe Relation
|
||||
toSourceRelation r@Relation{relTable=t}
|
||||
-- with {sourceCTEName}. This way findRel can find relationships with sourceCTEName.
|
||||
toSourceRel :: Relation -> Maybe Relation
|
||||
toSourceRel r@Relation{relTable=t}
|
||||
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
|
||||
| otherwise = Nothing
|
||||
|
||||
@@ -77,14 +73,14 @@ initReadRequest rootQi =
|
||||
rootName = qiName rootQi
|
||||
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
|
||||
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
||||
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) =
|
||||
treeEntry depth (Node fld@((fn, _),_,alias, embedHint) fldForest) (Node (q, i) rForest) =
|
||||
let nxtDepth = succ depth in
|
||||
case fldForest of
|
||||
[] -> Node (q {select=fld:select q}, i) rForest
|
||||
_ -> Node (q, i) $
|
||||
foldr (treeEntry nxtDepth)
|
||||
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
|
||||
(fn, Nothing, alias, relationDetail, nxtDepth)) [])
|
||||
(fn, Nothing, alias, embedHint, nxtDepth)) [])
|
||||
fldForest:rForest
|
||||
|
||||
treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
@@ -95,30 +91,16 @@ treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
|
||||
|
||||
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
augumentRequestWithJoin schema allRels request =
|
||||
addRelations schema allRels Nothing request
|
||||
addRels schema allRels Nothing request
|
||||
>>= addJoinConditions Nothing
|
||||
|
||||
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
|
||||
addRels :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, depth)) forest) =
|
||||
case parentNode of
|
||||
Just (Node (Select{from=parentNodeQi}, _) _) ->
|
||||
let newFrom r = if qiName tbl == nodeName then tableQi (relFTable r) else tbl
|
||||
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
|
||||
parentNodeTable = qiName parentNodeQi
|
||||
results = findRelation schema allRelations parentNodeTable nodeName relationDetail
|
||||
rel :: Either ApiRequestError Relation
|
||||
rel = case results of
|
||||
[] -> Left $ NoRelBetween parentNodeTable nodeName
|
||||
[r] -> Right r
|
||||
rs ->
|
||||
-- Hack for handling a self reference relationship.
|
||||
-- In this case we get an O2M and M2O rels with the same relTable and relFtable.
|
||||
-- We output the O2M rel, the M2O rel can be obtained by using the fk column as an embed hint in findRelation.
|
||||
let rel0 = head rs
|
||||
rel1 = rs !! 1 in
|
||||
if length rs == 2 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
|
||||
then note (NoRelBetween parentNodeTable nodeName) (find (\r -> relType r == O2M) rs)
|
||||
else Left $ AmbiguousRelBetween parentNodeTable nodeName rs
|
||||
rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
|
||||
in
|
||||
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
|
||||
_ ->
|
||||
@@ -126,126 +108,110 @@ addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (node
|
||||
Node rn <$> updateForest (Just $ Node rn forest)
|
||||
where
|
||||
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
||||
updateForest rq = mapM (addRelations schema allRelations rq) forest
|
||||
updateForest rq = mapM (addRels schema allRels rq) forest
|
||||
|
||||
findRelation :: Schema -> [Relation] -> TableName -> NodeName -> Maybe RelationDetail -> [Relation]
|
||||
findRelation schema allRelations parentTableName nodeName relationDetail =
|
||||
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
|
||||
-- Both relation ends need to be on the exposed schema
|
||||
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
||||
case relationDetail of
|
||||
Nothing ->
|
||||
|
||||
-- (request) => projects { ..., clients{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
-- Finds a relationship between an origin and a target in the request: /origin?select=target(*)
|
||||
-- If more than one relationship is found then the request is ambiguous and we return an error.
|
||||
-- In that case the request can be disambiguated by adding precision to the target or by using a hint: /origin?select=target!hint(*)
|
||||
-- The elements will be matched according to these rules:
|
||||
-- origin = table / view
|
||||
-- target = table / view / constraint / column-from-origin
|
||||
-- hint = table / view / constraint / column-from-origin / column-from-target
|
||||
-- (hint can take table / view values to aid in finding the junction in an m2m relationship)
|
||||
findRel :: Schema -> [Relation] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relation
|
||||
findRel schema allRels origin target hint =
|
||||
case rel of
|
||||
[] -> Left $ NoRelBetween origin target
|
||||
[r] -> Right r
|
||||
rs ->
|
||||
-- Return error if more than one relationship is found, unless we're in a self reference case.
|
||||
--
|
||||
-- Here we handle a self reference relationship to not cause a breaking change:
|
||||
-- In a self reference we get two relationships with the same foreign key and relTable/relFtable but with different cardinalities(m2o/o2m)
|
||||
-- We output the O2M rel, the M2O rel can be obtained by using the origin column as an embed hint.
|
||||
let [rel0, rel1] = take 2 rs in
|
||||
if length rs == 2 && relConstraint rel0 == relConstraint rel1 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
|
||||
then note (NoRelBetween origin target) (find (\r -> relType r == O2M) rs)
|
||||
else Left $ AmbiguousRelBetween origin target rs
|
||||
where
|
||||
matchFKSingleCol hint_ cols = length cols == 1 && hint_ == (colName <$> head cols)
|
||||
rel = filter (
|
||||
\Relation{relTable, relColumns, relConstraint, relFTable, relFColumns, relType, relJunction} ->
|
||||
-- Both relationship ends need to be on the exposed schema
|
||||
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
||||
(
|
||||
parentTableName == tableName relTable && -- projects
|
||||
nodeName == tableName relFTable -- clients
|
||||
) ||
|
||||
-- /projects?select=clients(*)
|
||||
origin == tableName relTable && -- projects
|
||||
target == tableName relFTable || -- clients
|
||||
|
||||
-- (request) => projects { ..., client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
parentTableName == tableName relTable && -- projects
|
||||
length relColumns == 1 &&
|
||||
-- match common foreign key names(table_name_id, table_name_fk) to table_name
|
||||
(toS ("^" <> colName (unsafeHead relColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~
|
||||
(toS nodeName :: BS.ByteString) -- client_id
|
||||
-- /projects?select=projects_client_id_fkey(*)
|
||||
(
|
||||
origin == tableName relTable && -- projects
|
||||
Just target == relConstraint -- projects_client_id_fkey
|
||||
) ||
|
||||
-- /projects?select=client_id(*)
|
||||
(
|
||||
origin == tableName relTable && -- projects
|
||||
matchFKSingleCol (Just target) relColumns -- client_id
|
||||
)
|
||||
) && (
|
||||
isNothing hint || -- hint is optional
|
||||
|
||||
-- /projects?select=clients!projects_client_id_fkey(*)
|
||||
hint == relConstraint || -- projects_client_id_fkey
|
||||
|
||||
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
|
||||
matchFKSingleCol hint relColumns || -- client_id
|
||||
matchFKSingleCol hint relFColumns || -- id
|
||||
|
||||
-- /users?select=tasks!users_tasks(*)
|
||||
(
|
||||
relType == M2M && -- many-to-many between users and tasks
|
||||
hint == (tableName . junTable <$> relJunction) -- users_tasks
|
||||
)
|
||||
)
|
||||
|
||||
-- (request) => project_id { ..., client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
-- this case works becasue before reaching this place
|
||||
-- addRelation will turn project_id to project so the above condition will match
|
||||
|
||||
Just rd ->
|
||||
|
||||
-- (request) => clients { ..., projects!client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => O2M
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
relType == O2M &&
|
||||
parentTableName == tableName relTable && -- clients
|
||||
nodeName == tableName relFTable && -- projects
|
||||
length relFColumns == 1 &&
|
||||
rd == colName (unsafeHead relFColumns) -- rd is client_id
|
||||
) ||
|
||||
|
||||
-- (request) => message { ..., person_detail!sender{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => message {sender}
|
||||
-- (foriegn entity) => person_detail {id}
|
||||
(
|
||||
relType == M2O &&
|
||||
parentTableName == tableName relTable && -- message
|
||||
nodeName == tableName relFTable && -- person_detail
|
||||
length relColumns == 1 &&
|
||||
rd == colName (unsafeHead relColumns) -- rd is sender
|
||||
) ||
|
||||
|
||||
-- (request) => tasks { ..., users.tasks_users{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2M
|
||||
-- (entity) => users
|
||||
-- (foriegn entity) => tasks
|
||||
(
|
||||
relType == M2M &&
|
||||
parentTableName == tableName relTable && -- tasks
|
||||
nodeName == tableName relFTable && -- users
|
||||
rd == tableName (fromJust relLinkTable) -- rd is tasks_users
|
||||
)
|
||||
) allRelations
|
||||
) allRels
|
||||
|
||||
-- previousAlias is only used for the case of self joins
|
||||
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
|
||||
case relation of
|
||||
Just rel@Relation{relType=O2M} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=M2O} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=M2M, relLinkTable=lTable} ->
|
||||
case lTable of
|
||||
Just linkTable ->
|
||||
let rq = augmentQuery rel in
|
||||
Node (rq{implicitJoins=tableQi linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
|
||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, depth)) forest) =
|
||||
case rel of
|
||||
Just r@Relation{relType=O2M} -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
||||
Just r@Relation{relType=M2O} -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
||||
Just r@Relation{relType=M2M, relJunction=junction} ->
|
||||
case junction of
|
||||
Just Junction{junTable} ->
|
||||
let rq = augmentQuery r in
|
||||
Node (rq{implicitJoins=tableQi junTable:implicitJoins rq}, nodeProps) <$> updatedForest
|
||||
Nothing ->
|
||||
Left UnknownRelation
|
||||
Nothing -> Node node <$> updatedForest
|
||||
where
|
||||
newAlias = case isSelfReference <$> relation of
|
||||
newAlias = case isSelfReference <$> rel of
|
||||
Just True
|
||||
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
|
||||
| otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
augmentQuery rel =
|
||||
augmentQuery r =
|
||||
foldr
|
||||
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
|
||||
query{fromAlias=newAlias}
|
||||
(getJoinConditions previousAlias newAlias rel)
|
||||
(getJoinConditions previousAlias newAlias r)
|
||||
updatedForest = mapM (addJoinConditions newAlias) forest
|
||||
|
||||
-- previousAlias and newAlias are used in the case of self joins
|
||||
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition]
|
||||
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
|
||||
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols _ Table{tableName=ftN} fCols typ jun) =
|
||||
case typ of
|
||||
O2M ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
M2O ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
M2M ->
|
||||
let ltN = maybe "" tableName lt in
|
||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
||||
M2M -> case jun of
|
||||
Just (Junction jt _ jc1 _ jc2) ->
|
||||
let jtn = tableName jt in
|
||||
zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
|
||||
Nothing -> []
|
||||
where
|
||||
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
|
||||
toJoinCondition tb ftb c fc =
|
||||
|
||||
@@ -95,9 +95,10 @@ decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
|
||||
decodeRels tables cols =
|
||||
mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
|
||||
where
|
||||
relRow = (,,,,,)
|
||||
relRow = (,,,,,,)
|
||||
<$> column HD.text
|
||||
<*> column HD.text
|
||||
<*> column HD.text
|
||||
<*> column (HD.array (HD.dimension replicateM (element HD.text)))
|
||||
<*> column HD.text
|
||||
<*> column HD.text
|
||||
@@ -290,7 +291,7 @@ The logic for composite pks is similar just need to make sure all the Relation c
|
||||
addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation]
|
||||
addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||
rel : case rel of
|
||||
Relation{relType=M2O, relTable, relColumns, relFTable, relFColumns} ->
|
||||
Relation{relType=M2O, relTable, relColumns, relConstraint, relFTable, relFColumns} ->
|
||||
|
||||
let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
|
||||
srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
|
||||
@@ -307,20 +308,22 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||
|
||||
viewTableM2O =
|
||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||
relFTable relFColumns
|
||||
M2O Nothing Nothing Nothing
|
||||
relConstraint relFTable relFColumns
|
||||
M2O Nothing
|
||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
|
||||
|
||||
tableViewM2O =
|
||||
[ Relation relTable relColumns
|
||||
relConstraint
|
||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||
M2O Nothing Nothing Nothing
|
||||
M2O Nothing
|
||||
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||
|
||||
viewViewM2O =
|
||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||
relConstraint
|
||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||
M2O Nothing Nothing Nothing
|
||||
M2O Nothing
|
||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
|
||||
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||
|
||||
@@ -329,12 +332,12 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||
_ -> [])
|
||||
|
||||
addO2MRels :: [Relation] -> [Relation]
|
||||
addO2MRels = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c O2M Nothing Nothing Nothing])
|
||||
addO2MRels = concatMap (\rel@(Relation t c cn ft fc _ _) -> [rel, Relation ft fc cn t c O2M Nothing])
|
||||
|
||||
addM2MRels :: [Relation] -> [Relation]
|
||||
addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
||||
addM2MRels rels = rels ++ addMirrorRel (mapMaybe junction2Rel junctions)
|
||||
where
|
||||
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==M2O). relType) rels
|
||||
junctions = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==M2O). relType) rels
|
||||
groupFn :: Relation -> Text
|
||||
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
|
||||
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
|
||||
@@ -342,14 +345,15 @@ addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
||||
combinations 0 _ = [ [] ]
|
||||
combinations n xs = [ y:ys | y:xs' <- tails xs
|
||||
, ys <- combinations (n-1) xs']
|
||||
addMirrorRelation = concatMap (\rel@(Relation t c ft fc _ lt lc1 lc2) -> [rel, Relation ft fc t c M2M lt lc2 lc1])
|
||||
link2Relation [
|
||||
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
|
||||
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
|
||||
junction2Rel [
|
||||
Relation{relTable=jt, relColumns=jc1, relConstraint=const1, relFTable=t, relFColumns=c},
|
||||
Relation{ relColumns=jc2, relConstraint=const2, relFTable=ft, relFColumns=fc}
|
||||
]
|
||||
| lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc M2M (Just lt) (Just lc1) (Just lc2)
|
||||
| jc1 /= jc2 && length jc1 == 1 && length jc2 == 1 = Just $ Relation t c Nothing ft fc M2M (Just $ Junction jt const1 jc1 const2 jc2)
|
||||
| otherwise = Nothing
|
||||
link2Relation _ = Nothing
|
||||
junction2Rel _ = Nothing
|
||||
addMirrorRel = concatMap (\rel@(Relation t c _ ft fc _ (Just (Junction jt const1 jc1 const2 jc2))) ->
|
||||
[rel, Relation ft fc Nothing t c M2M (Just (Junction jt const2 jc2 const1 jc1))])
|
||||
|
||||
addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey]
|
||||
addViewPrimaryKeys srcCols = concatMap (\pk ->
|
||||
@@ -574,32 +578,29 @@ allM2ORels tabs cols =
|
||||
sql = [q|
|
||||
SELECT ns1.nspname AS table_schema,
|
||||
tab.relname AS table_name,
|
||||
conname AS constraint_name,
|
||||
column_info.cols AS columns,
|
||||
ns2.nspname AS foreign_table_schema,
|
||||
other.relname AS foreign_table_name,
|
||||
column_info.refs AS foreign_columns
|
||||
FROM pg_constraint,
|
||||
LATERAL (SELECT array_agg(cols.attname) AS cols,
|
||||
array_agg(cols.attnum) AS nums,
|
||||
array_agg(refs.attname) AS refs
|
||||
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
||||
LATERAL (SELECT * FROM pg_attribute
|
||||
WHERE attrelid = conrelid AND attnum = col)
|
||||
AS cols,
|
||||
LATERAL (SELECT * FROM pg_attribute
|
||||
WHERE attrelid = confrelid AND attnum = ref)
|
||||
AS refs)
|
||||
AS column_info,
|
||||
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
|
||||
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
|
||||
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
|
||||
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
|
||||
LATERAL (
|
||||
SELECT array_agg(cols.attname) AS cols,
|
||||
array_agg(cols.attnum) AS nums,
|
||||
array_agg(refs.attname) AS refs
|
||||
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
|
||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
|
||||
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
|
||||
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
|
||||
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
|
||||
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
|
||||
WHERE confrelid != 0
|
||||
ORDER BY (conrelid, column_info.nums) |]
|
||||
|
||||
relFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
||||
relFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
|
||||
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure M2O <*> pure Nothing <*> pure Nothing <*> pure Nothing
|
||||
relFromRow :: [Table] -> [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
||||
relFromRow allTabs allCols (rs, rt, cn, rcs, frs, frt, frcs) =
|
||||
Relation <$> table <*> cols <*> pure (Just cn) <*> tableF <*> colsF <*> pure M2O <*> pure Nothing
|
||||
where
|
||||
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
|
||||
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
|
||||
|
||||
+14
-18
@@ -80,9 +80,9 @@ instance JSON.ToJSON ApiRequestError where
|
||||
toJSON UnknownRelation = JSON.object [
|
||||
"message" .= ("Unknown relation" :: Text)]
|
||||
toJSON (NoRelBetween parent child) = JSON.object [
|
||||
"message" .= ("Could not find foreign keys between these entities, No relation found between " <> parent <> " and " <> child :: Text)]
|
||||
"message" .= ("Could not find foreign keys between these entities. No relationship found between " <> parent <> " and " <> child :: Text)]
|
||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||
"hint" .= ("Disambiguate by choosing a relationship from the `details` key" :: Text),
|
||||
"hint" .= ("By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)" :: Text),
|
||||
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
|
||||
"details" .= (compressedRel <$> rels) ]
|
||||
toJSON UnsupportedVerb = JSON.object [
|
||||
@@ -93,27 +93,23 @@ instance JSON.ToJSON ApiRequestError where
|
||||
compressedRel :: Relation -> JSON.Value
|
||||
compressedRel rel =
|
||||
let
|
||||
-- | Format like "test.orders[billing_address_id]". For easier debugging the format is compressed instead of structured.
|
||||
fmt sch tbl cols = schTbl sch tbl <> joinCols cols
|
||||
fmtMany sch tbl cols1 cols2 = schTbl sch tbl <> joinCols cols1 <> joinCols cols2
|
||||
schTbl sch tbl = sch <> "." <> tbl
|
||||
joinCols cols = "[" <> T.intercalate ", " cols <> "]"
|
||||
|
||||
tab = relTable rel
|
||||
fTab = relFTable rel
|
||||
fmtTbl tbl = tableSchema tbl <> "." <> tableName tbl
|
||||
fmtEls els = "[" <> T.intercalate ", " els <> "]"
|
||||
in
|
||||
JSON.object $ [
|
||||
"source" .= fmt (tableSchema tab) (tableName tab) (colName <$> relColumns rel)
|
||||
, "target" .= fmt (tableSchema fTab) (tableName fTab) (colName <$> relFColumns rel)
|
||||
"origin" .= fmtTbl (relTable rel)
|
||||
, "target" .= fmtTbl (relFTable rel)
|
||||
, "cardinality" .= (show $ relType rel :: Text)
|
||||
] ++
|
||||
if relType rel == M2M
|
||||
then [
|
||||
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of
|
||||
(Just lt, Just lc1, Just lc2) -> fmtMany (tableSchema lt) (tableName lt) (colName <$> lc1) (colName <$> lc2)
|
||||
_ -> toS $ JSON.encode JSON.Null
|
||||
case (relType rel, relJunction rel, relConstraint rel) of
|
||||
(M2M, Just (Junction jt (Just const1) _ (Just const2) _), _) -> [
|
||||
"relationship" .= (fmtTbl jt <> fmtEls [const1] <> fmtEls [const2])
|
||||
]
|
||||
else mempty
|
||||
(_, _, Just relCon) -> [
|
||||
"relationship" .= (relCon <> fmtEls (colName <$> relColumns rel) <> fmtEls (colName <$> relFColumns rel))
|
||||
]
|
||||
(_, _, _) ->
|
||||
mempty
|
||||
|
||||
data PgError = PgError Authenticated P.UsageError
|
||||
type Authenticated = Bool
|
||||
|
||||
@@ -134,12 +134,12 @@ pRelationSelect :: Parser SelectItem
|
||||
pRelationSelect = lexeme $ try ( do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
relationDetail <- optionMaybe (
|
||||
try ( char '!' *> pFieldName ) <|>
|
||||
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version
|
||||
hint <- optionMaybe (
|
||||
try ( char '!' *> pFieldName) <|>
|
||||
-- deprecated, remove in next major version
|
||||
try ( char '.' *> pFieldName)
|
||||
)
|
||||
|
||||
return (fld, Nothing, alias, relationDetail)
|
||||
return (fld, Nothing, alias, hint)
|
||||
)
|
||||
|
||||
pFieldSelect :: Parser SelectItem
|
||||
|
||||
+28
-20
@@ -264,28 +264,34 @@ data Cardinality = O2M -- ^ one-to-many, previously known as Parent
|
||||
| M2M -- ^ many-to-many, previously known as Many
|
||||
deriving Eq
|
||||
instance Show Cardinality where
|
||||
show O2M = "one-to-many"
|
||||
show M2O = "many-to-one"
|
||||
show M2M = "many-to-many"
|
||||
show O2M = "o2m"
|
||||
show M2O = "m2o"
|
||||
show M2M = "m2m"
|
||||
|
||||
type ConstraintName = Text
|
||||
|
||||
{-|
|
||||
The name 'Relation' here is used with the meaning
|
||||
"What is the relation between the current node and the parent node".
|
||||
It has nothing to do with PostgreSQL referring to tables/views as relations.
|
||||
The order of the relColumns and relFColumns should be maintained to get
|
||||
the join conditions right.
|
||||
"Relation"ship between two tables.
|
||||
The order of the relColumns and relFColumns should be maintained to get the join conditions right.
|
||||
TODO merge relColumns and relFColumns to a tuple or Data.Bimap
|
||||
-}
|
||||
data Relation = Relation {
|
||||
relTable :: Table
|
||||
, relColumns :: [Column]
|
||||
, relFTable :: Table
|
||||
, relFColumns :: [Column]
|
||||
, relType :: Cardinality
|
||||
-- The Link attrs are used when Cardinality == Many
|
||||
, relLinkTable :: Maybe Table
|
||||
, relLinkCols1 :: Maybe [Column]
|
||||
, relLinkCols2 :: Maybe [Column]
|
||||
relTable :: Table
|
||||
, relColumns :: [Column]
|
||||
, relConstraint :: Maybe ConstraintName -- ^ Just on O2M/M2O, Nothing on M2M
|
||||
, relFTable :: Table
|
||||
, relFColumns :: [Column]
|
||||
, relType :: Cardinality
|
||||
, relJunction :: Maybe Junction -- ^ Junction for M2M Cardinality
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- | Junction table on an M2M relationship
|
||||
data Junction = Junction {
|
||||
junTable :: Table
|
||||
, junConstraint1 :: Maybe ConstraintName
|
||||
, junCols1 :: [Column]
|
||||
, junConstraint2 :: Maybe ConstraintName
|
||||
, junCols2 :: [Column]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
isSelfReference :: Relation -> Bool
|
||||
@@ -409,8 +415,10 @@ toHeaders = map $ \(GucHeader (k, v)) -> (CI.mk $ toS k, toS v)
|
||||
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones.
|
||||
Specifically, it will contain the name of the foreign key or the join table in many to many relations.
|
||||
-}
|
||||
type RelationDetail = Text
|
||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail)
|
||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
||||
-- | Disambiguates an embedding operation when there's multiple relationships between two tables.
|
||||
-- | Can be the name of a foreign key constraint, column name or the junction in an m2m relationship.
|
||||
type EmbedHint = Text
|
||||
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
|
||||
type EmbedPath = [Text]
|
||||
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
|
||||
@@ -453,7 +461,7 @@ data MutateQuery =
|
||||
type ReadRequest = Tree ReadNode
|
||||
type MutateRequest = MutateQuery
|
||||
|
||||
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth))
|
||||
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe EmbedHint, Depth))
|
||||
type Depth = Integer
|
||||
|
||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
||||
|
||||
Reference in New Issue
Block a user