mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
fix postgres query error when computed fields included in mutation response, fix #4035
This commit is contained in:
parent
db724f719d
commit
9128c69a80
@ -457,7 +457,7 @@ insertMultipleObjects strfyNum role tn multiObjIns addCols mutOutput errP =
|
|||||||
let affRows = sum $ map fst insResps
|
let affRows = sum $ map fst insResps
|
||||||
columnValues = catMaybes $ map snd insResps
|
columnValues = catMaybes $ map snd insResps
|
||||||
cteExp <- mkSelCTEFromColVals tn tableColInfos columnValues
|
cteExp <- mkSelCTEFromColVals tn tableColInfos columnValues
|
||||||
let sql = toSQL $ RR.mkMutationOutputExp tn (Just affRows) cteExp mutOutput strfyNum
|
let sql = toSQL $ RR.mkMutationOutputExp tn tableColInfos (Just affRows) cteExp mutOutput strfyNum
|
||||||
runIdentity . Q.getRow
|
runIdentity . Q.getRow
|
||||||
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) [] False
|
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) [] False
|
||||||
|
|
||||||
|
@ -38,19 +38,19 @@ runMutation mut =
|
|||||||
hasNestedFld $ _mOutput mut
|
hasNestedFld $ _mOutput mut
|
||||||
|
|
||||||
mutateAndReturn :: Mutation -> Q.TxE QErr EncJSON
|
mutateAndReturn :: Mutation -> Q.TxE QErr EncJSON
|
||||||
mutateAndReturn (Mutation qt (cte, p) mutationOutput _ strfyNum) =
|
mutateAndReturn (Mutation qt (cte, p) mutationOutput allCols strfyNum) =
|
||||||
encJFromBS . runIdentity . Q.getRow
|
encJFromBS . runIdentity . Q.getRow
|
||||||
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith)
|
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith)
|
||||||
(toList p) True
|
(toList p) True
|
||||||
where
|
where
|
||||||
selWith = mkMutationOutputExp qt Nothing cte mutationOutput strfyNum
|
selWith = mkMutationOutputExp qt allCols Nothing cte mutationOutput strfyNum
|
||||||
|
|
||||||
mutateAndSel :: Mutation -> Q.TxE QErr EncJSON
|
mutateAndSel :: Mutation -> Q.TxE QErr EncJSON
|
||||||
mutateAndSel (Mutation qt q mutationOutput allCols strfyNum) = do
|
mutateAndSel (Mutation qt q mutationOutput allCols strfyNum) = do
|
||||||
-- Perform mutation and fetch unique columns
|
-- Perform mutation and fetch unique columns
|
||||||
MutateResp _ columnVals <- mutateAndFetchCols qt allCols q strfyNum
|
MutateResp _ columnVals <- mutateAndFetchCols qt allCols q strfyNum
|
||||||
selCTE <- mkSelCTEFromColVals qt allCols columnVals
|
selCTE <- mkSelCTEFromColVals qt allCols columnVals
|
||||||
let selWith = mkMutationOutputExp qt Nothing selCTE mutationOutput strfyNum
|
let selWith = mkMutationOutputExp qt allCols Nothing selCTE mutationOutput strfyNum
|
||||||
-- Perform select query and fetch returning fields
|
-- Perform select query and fetch returning fields
|
||||||
encJFromBS . runIdentity . Q.getRow
|
encJFromBS . runIdentity . Q.getRow
|
||||||
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) [] True
|
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) [] True
|
||||||
@ -108,8 +108,7 @@ mkSelCTEFromColVals qt allCols colVals =
|
|||||||
where
|
where
|
||||||
rowAlias = Iden "row"
|
rowAlias = Iden "row"
|
||||||
extractor = S.selectStar' $ S.QualIden rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt
|
extractor = S.selectStar' $ S.QualIden rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt
|
||||||
sortedCols = flip sortBy allCols $ \lCol rCol ->
|
sortedCols = sortCols allCols
|
||||||
compare (pgiPosition lCol) (pgiPosition rCol)
|
|
||||||
mkTupsFromColVal colVal =
|
mkTupsFromColVal colVal =
|
||||||
fmap S.TupleExp $ forM sortedCols $ \ci -> do
|
fmap S.TupleExp $ forM sortedCols $ \ci -> do
|
||||||
let pgCol = pgiColumn ci
|
let pgCol = pgiColumn ci
|
||||||
|
@ -93,12 +93,8 @@ mkDefaultMutFlds = MOutMultirowFields . \case
|
|||||||
where
|
where
|
||||||
mutFlds = [("affected_rows", MCount)]
|
mutFlds = [("affected_rows", MCount)]
|
||||||
|
|
||||||
qualTableToAliasIden :: QualifiedTable -> Iden
|
mkMutFldExp :: Iden -> Maybe Int -> Bool -> MutFld -> S.SQLExp
|
||||||
qualTableToAliasIden qt =
|
mkMutFldExp cteAlias preCalAffRows strfyNum = \case
|
||||||
Iden $ snakeCaseTable qt <> "__mutation_result_alias"
|
|
||||||
|
|
||||||
mkMutFldExp :: QualifiedTable -> Maybe Int -> Bool -> MutFld -> S.SQLExp
|
|
||||||
mkMutFldExp qt preCalAffRows strfyNum = \case
|
|
||||||
MCount ->
|
MCount ->
|
||||||
let countExp = S.SESelect $
|
let countExp = S.SESelect $
|
||||||
S.mkSelect
|
S.mkSelect
|
||||||
@ -112,28 +108,70 @@ mkMutFldExp qt preCalAffRows strfyNum = \case
|
|||||||
tabPerm = TablePerm annBoolExpTrue Nothing
|
tabPerm = TablePerm annBoolExpTrue Nothing
|
||||||
in S.SESelect $ mkSQLSelect JASMultipleRows $
|
in S.SESelect $ mkSQLSelect JASMultipleRows $
|
||||||
AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum
|
AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum
|
||||||
where
|
|
||||||
cteAlias = qualTableToAliasIden qt
|
|
||||||
|
|
||||||
|
{- Note [Mutation output expression]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
An example output expression for INSERT mutation:
|
||||||
|
|
||||||
|
WITH "<table-name>__mutation_result_alias" AS (
|
||||||
|
INSERT INTO <table-name> (<insert-column>[..])
|
||||||
|
VALUES
|
||||||
|
(<insert-value-row>[..])
|
||||||
|
ON CONFLICT ON CONSTRAINT "<table-constraint-name>" DO NOTHING RETURNING *,
|
||||||
|
-- An extra column expression which performs the 'CHECK' validation
|
||||||
|
CASE
|
||||||
|
WHEN (<CHECK Condition>) THEN NULL
|
||||||
|
ELSE "hdb_catalog"."check_violation"('insert check constraint failed')
|
||||||
|
END
|
||||||
|
),
|
||||||
|
"<table-name>__all_columns_alias" AS (
|
||||||
|
-- Only extract columns from mutated rows. Columns sorted by ordinal position so that
|
||||||
|
-- resulted rows can be casted to table type.
|
||||||
|
SELECT (<table-column>[..])
|
||||||
|
FROM
|
||||||
|
"<table-name>__mutation_result_alias"
|
||||||
|
)
|
||||||
|
<SELECT statement to generate mutation response using '<table-name>__all_columns_alias' as FROM>
|
||||||
|
-}
|
||||||
|
|
||||||
|
-- | Generate mutation output expression with given mutation CTE statement.
|
||||||
|
-- See Note [Mutation output expression].
|
||||||
mkMutationOutputExp
|
mkMutationOutputExp
|
||||||
:: QualifiedTable -> Maybe Int -> S.CTE -> MutationOutput -> Bool -> S.SelectWith
|
:: QualifiedTable
|
||||||
mkMutationOutputExp qt preCalAffRows cte mutOutput strfyNum =
|
-> [PGColumnInfo]
|
||||||
S.SelectWith [(S.Alias cteAlias, cte)] sel
|
-> Maybe Int
|
||||||
|
-> S.CTE
|
||||||
|
-> MutationOutput
|
||||||
|
-> Bool
|
||||||
|
-> S.SelectWith
|
||||||
|
mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
|
||||||
|
S.SelectWith [ (S.Alias mutationResultAlias, cte)
|
||||||
|
, (S.Alias allColumnsAlias, allColumnsSelect)
|
||||||
|
] sel
|
||||||
where
|
where
|
||||||
cteAlias = qualTableToAliasIden qt
|
mutationResultAlias = Iden $ snakeCaseQualObject qt <> "__mutation_result_alias"
|
||||||
|
allColumnsAlias = Iden $ snakeCaseQualObject qt <> "__all_columns_alias"
|
||||||
|
|
||||||
|
allColumnsSelect = S.CTESelect $ S.mkSelect
|
||||||
|
{ S.selExtr = map S.mkExtr $ map pgiColumn $ sortCols allCols
|
||||||
|
, S.selFrom = Just $ S.mkIdenFromExp mutationResultAlias
|
||||||
|
}
|
||||||
|
|
||||||
sel = S.mkSelect { S.selExtr = [S.Extractor extrExp Nothing] }
|
sel = S.mkSelect { S.selExtr = [S.Extractor extrExp Nothing] }
|
||||||
|
where
|
||||||
|
extrExp = case mutOutput of
|
||||||
|
MOutMultirowFields mutFlds ->
|
||||||
|
let jsonBuildObjArgs = flip concatMap mutFlds $
|
||||||
|
\(FieldName k, mutFld) -> [ S.SELit k
|
||||||
|
, mkMutFldExp allColumnsAlias preCalAffRows strfyNum mutFld
|
||||||
|
]
|
||||||
|
in S.SEFnApp "json_build_object" jsonBuildObjArgs Nothing
|
||||||
|
|
||||||
extrExp = case mutOutput of
|
MOutSinglerowObject annFlds ->
|
||||||
MOutMultirowFields mutFlds ->
|
let tabFrom = FromIden allColumnsAlias
|
||||||
let jsonBuildObjArgs = flip concatMap mutFlds $
|
tabPerm = TablePerm annBoolExpTrue Nothing
|
||||||
\(FieldName k, mutFld) -> [S.SELit k, mkMutFldExp qt preCalAffRows strfyNum mutFld]
|
in S.SESelect $ mkSQLSelect JASSingleObject $
|
||||||
in S.SEFnApp "json_build_object" jsonBuildObjArgs Nothing
|
AnnSelG annFlds tabFrom tabPerm noTableArgs strfyNum
|
||||||
|
|
||||||
MOutSinglerowObject annFlds ->
|
|
||||||
let tabFrom = FromIden cteAlias
|
|
||||||
tabPerm = TablePerm annBoolExpTrue Nothing
|
|
||||||
in S.SESelect $ mkSQLSelect JASSingleObject $
|
|
||||||
AnnSelG annFlds tabFrom tabPerm noTableArgs strfyNum
|
|
||||||
|
|
||||||
|
|
||||||
checkRetCols
|
checkRetCols
|
||||||
|
@ -337,7 +337,7 @@ convColRhs tableQual = \case
|
|||||||
curVarNum <- get
|
curVarNum <- get
|
||||||
put $ curVarNum + 1
|
put $ curVarNum + 1
|
||||||
let newIden = Iden $ "_be_" <> T.pack (show curVarNum) <> "_"
|
let newIden = Iden $ "_be_" <> T.pack (show curVarNum) <> "_"
|
||||||
<> snakeCaseTable relTN
|
<> snakeCaseQualObject relTN
|
||||||
newIdenQ = S.QualIden newIden Nothing
|
newIdenQ = S.QualIden newIden Nothing
|
||||||
annRelBoolExp <- convBoolRhs' newIdenQ nesAnn
|
annRelBoolExp <- convBoolRhs' newIdenQ nesAnn
|
||||||
let backCompExp = foldr (S.BEBin S.AndOp) (S.BELit True) $
|
let backCompExp = foldr (S.BEBin S.AndOp) (S.BELit True) $
|
||||||
|
@ -44,6 +44,7 @@ module Hasura.RQL.Types.Table
|
|||||||
, getFieldInfoM
|
, getFieldInfoM
|
||||||
, getPGColumnInfoM
|
, getPGColumnInfoM
|
||||||
, getCols
|
, getCols
|
||||||
|
, sortCols
|
||||||
, getRels
|
, getRels
|
||||||
, getComputedFieldInfos
|
, getComputedFieldInfos
|
||||||
|
|
||||||
@ -193,6 +194,10 @@ fieldInfoGraphQLNames = \case
|
|||||||
getCols :: FieldInfoMap FieldInfo -> [PGColumnInfo]
|
getCols :: FieldInfoMap FieldInfo -> [PGColumnInfo]
|
||||||
getCols = mapMaybe (^? _FIColumn) . M.elems
|
getCols = mapMaybe (^? _FIColumn) . M.elems
|
||||||
|
|
||||||
|
-- | Sort columns based on their ordinal position
|
||||||
|
sortCols :: [PGColumnInfo] -> [PGColumnInfo]
|
||||||
|
sortCols = sortBy (\l r -> compare (pgiPosition l) (pgiPosition r))
|
||||||
|
|
||||||
getRels :: FieldInfoMap FieldInfo -> [RelInfo]
|
getRels :: FieldInfoMap FieldInfo -> [RelInfo]
|
||||||
getRels = mapMaybe (^? _FIRelationship) . M.elems
|
getRels = mapMaybe (^? _FIRelationship) . M.elems
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ module Hasura.SQL.Types
|
|||||||
, isView
|
, isView
|
||||||
|
|
||||||
, QualifiedTable
|
, QualifiedTable
|
||||||
, snakeCaseTable
|
|
||||||
, QualifiedFunction
|
, QualifiedFunction
|
||||||
|
|
||||||
, PGDescription(..)
|
, PGDescription(..)
|
||||||
@ -300,10 +299,6 @@ snakeCaseQualObject (QualifiedObject sn o)
|
|||||||
|
|
||||||
type QualifiedTable = QualifiedObject TableName
|
type QualifiedTable = QualifiedObject TableName
|
||||||
|
|
||||||
snakeCaseTable :: QualifiedObject TableName -> T.Text
|
|
||||||
snakeCaseTable (QualifiedObject sn tn) =
|
|
||||||
getSchemaTxt sn <> "_" <> getTableTxt tn
|
|
||||||
|
|
||||||
type QualifiedFunction = QualifiedObject FunctionName
|
type QualifiedFunction = QualifiedObject FunctionName
|
||||||
|
|
||||||
newtype PGDescription
|
newtype PGDescription
|
||||||
|
@ -30,6 +30,11 @@ query:
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
is_registered
|
is_registered
|
||||||
|
get_articles(args: {search: "Article"}){
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,31 @@ args:
|
|||||||
author_id INTEGER REFERENCES author(id),
|
author_id INTEGER REFERENCES author(id),
|
||||||
is_published BOOLEAN,
|
is_published BOOLEAN,
|
||||||
published_on TIMESTAMP
|
published_on TIMESTAMP
|
||||||
)
|
);
|
||||||
|
|
||||||
|
CREATE FUNCTION fetch_articles(search text, author_row author)
|
||||||
|
RETURNS SETOF article AS $$
|
||||||
|
SELECT *
|
||||||
|
FROM article
|
||||||
|
WHERE
|
||||||
|
( title ilike ('%' || search || '%')
|
||||||
|
OR content ilike ('%' || search || '%')
|
||||||
|
) AND author_id = author_row.id
|
||||||
|
$$ LANGUAGE sql STABLE;
|
||||||
|
|
||||||
- type: track_table
|
- type: track_table
|
||||||
args:
|
args:
|
||||||
schema: public
|
schema: public
|
||||||
name: article
|
name: article
|
||||||
|
|
||||||
|
- type: add_computed_field
|
||||||
|
args:
|
||||||
|
table: author
|
||||||
|
name: get_articles
|
||||||
|
definition:
|
||||||
|
function: fetch_articles
|
||||||
|
table_argument: author_row
|
||||||
|
|
||||||
#Create resident table
|
#Create resident table
|
||||||
- type: run_sql
|
- type: run_sql
|
||||||
args:
|
args:
|
||||||
|
@ -13,6 +13,7 @@ args:
|
|||||||
sql: |
|
sql: |
|
||||||
drop table address;
|
drop table address;
|
||||||
drop table resident;
|
drop table resident;
|
||||||
|
drop function fetch_articles(text,author);
|
||||||
drop table article;
|
drop table article;
|
||||||
drop table blog;
|
drop table blog;
|
||||||
drop table author;
|
drop table author;
|
||||||
|
@ -27,12 +27,30 @@ args:
|
|||||||
is_published BOOLEAN,
|
is_published BOOLEAN,
|
||||||
published_on TIMESTAMP,
|
published_on TIMESTAMP,
|
||||||
PRIMARY KEY (id,version)
|
PRIMARY KEY (id,version)
|
||||||
)
|
);
|
||||||
|
|
||||||
|
CREATE FUNCTION fetch_articles(search text, author_row author)
|
||||||
|
RETURNS SETOF article AS $$
|
||||||
|
SELECT *
|
||||||
|
FROM article
|
||||||
|
WHERE
|
||||||
|
( title ilike ('%' || search || '%')
|
||||||
|
OR content ilike ('%' || search || '%')
|
||||||
|
) AND author_id = author_row.id
|
||||||
|
$$ LANGUAGE sql STABLE;
|
||||||
|
|
||||||
- type: track_table
|
- type: track_table
|
||||||
args:
|
args:
|
||||||
schema: public
|
schema: public
|
||||||
name: article
|
name: article
|
||||||
|
|
||||||
|
- type: add_computed_field
|
||||||
|
args:
|
||||||
|
table: author
|
||||||
|
name: get_articles
|
||||||
|
definition:
|
||||||
|
function: fetch_articles
|
||||||
|
table_argument: author_row
|
||||||
|
|
||||||
#Object relationship
|
#Object relationship
|
||||||
- type: create_object_relationship
|
- type: create_object_relationship
|
||||||
@ -77,6 +95,17 @@ args:
|
|||||||
filter:
|
filter:
|
||||||
id: X-HASURA-USER-ID
|
id: X-HASURA-USER-ID
|
||||||
|
|
||||||
|
#Author update permission for user
|
||||||
|
- type: create_update_permission
|
||||||
|
args:
|
||||||
|
table: author
|
||||||
|
role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- name
|
||||||
|
filter:
|
||||||
|
id: X-Hasura-User-Id
|
||||||
|
|
||||||
#Article select permission for user
|
#Article select permission for user
|
||||||
- type: create_select_permission
|
- type: create_select_permission
|
||||||
args:
|
args:
|
||||||
|
@ -4,6 +4,7 @@ args:
|
|||||||
- type: run_sql
|
- type: run_sql
|
||||||
args:
|
args:
|
||||||
sql: |
|
sql: |
|
||||||
|
DROP function fetch_articles(text,author);
|
||||||
DROP table article;
|
DROP table article;
|
||||||
DROP table author;
|
DROP table author;
|
||||||
DROP table person;
|
DROP table person;
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
description: Update name of an author
|
||||||
|
url: /v1/graphql
|
||||||
|
status: 200
|
||||||
|
headers:
|
||||||
|
X-Hasura-Role: user
|
||||||
|
X-Hasura-User-Id: '1'
|
||||||
|
response:
|
||||||
|
data:
|
||||||
|
update_author:
|
||||||
|
affected_rows: 1
|
||||||
|
returning:
|
||||||
|
- id: 1
|
||||||
|
name: Author 1 updated
|
||||||
|
get_articles:
|
||||||
|
- id: 2
|
||||||
|
title: Article 1
|
||||||
|
content: Sample article 1, content version 1.0.1
|
||||||
|
query:
|
||||||
|
query: |
|
||||||
|
mutation {
|
||||||
|
update_author(
|
||||||
|
_set: {name: "Author 1 updated"}
|
||||||
|
where: {id: {_eq: 1}}
|
||||||
|
){
|
||||||
|
affected_rows
|
||||||
|
returning{
|
||||||
|
id
|
||||||
|
name
|
||||||
|
get_articles(args: {search: "1.0.1"}){
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -380,6 +380,9 @@ class TestGraphqlUpdateJsonB:
|
|||||||
@use_mutation_fixtures
|
@use_mutation_fixtures
|
||||||
class TestGraphqlUpdatePermissions:
|
class TestGraphqlUpdatePermissions:
|
||||||
|
|
||||||
|
def test_user_update_author(self, hge_ctx, transport):
|
||||||
|
check_query_f(hge_ctx, self.dir() + "/user_update_author.yaml", transport)
|
||||||
|
|
||||||
def test_user_can_update_unpublished_article(self, hge_ctx, transport):
|
def test_user_can_update_unpublished_article(self, hge_ctx, transport):
|
||||||
check_query_f(hge_ctx, self.dir() + "/user_can_update_unpublished_article.yaml")
|
check_query_f(hge_ctx, self.dir() + "/user_can_update_unpublished_article.yaml")
|
||||||
|
|
||||||
@ -388,7 +391,7 @@ class TestGraphqlUpdatePermissions:
|
|||||||
|
|
||||||
def test_user_cannot_update_another_users_article(self, hge_ctx, transport):
|
def test_user_cannot_update_another_users_article(self, hge_ctx, transport):
|
||||||
check_query_f(hge_ctx, self.dir() + "/user_cannot_update_another_users_article.yaml")
|
check_query_f(hge_ctx, self.dir() + "/user_cannot_update_another_users_article.yaml")
|
||||||
|
|
||||||
def test_user_cannot_publish(self, hge_ctx, transport):
|
def test_user_cannot_publish(self, hge_ctx, transport):
|
||||||
check_query_f(hge_ctx, self.dir() + "/user_cannot_publish.yaml")
|
check_query_f(hge_ctx, self.dir() + "/user_cannot_publish.yaml")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user