Fix action relationship type and input arguments (closes #6402) (#284)

Co-authored-by: Antoine Leblanc <antoine@hasura.io>
GITHUB_PR_NUMBER: 6417
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/6417
GitOrigin-RevId: 37b67a4d04e0ed3b16fc5fc9bf025b24b1f1bf6e
This commit is contained in:
hasura-bot 2021-01-18 12:26:25 +05:30
parent 647504ef99
commit 513a3d0c19
7 changed files with 237 additions and 23 deletions

View File

@ -88,6 +88,8 @@ and be accessible according to the permissions that were configured for the role
- server: various changes to ensure timely cleanup of background threads and other resources in the event of a SIGTERM signal. - server: various changes to ensure timely cleanup of background threads and other resources in the event of a SIGTERM signal.
- server: fix issue when the `relationships` field in `objects` field is passed `[]` in the `set_custom_types` API (fix #6357) - server: fix issue when the `relationships` field in `objects` field is passed `[]` in the `set_custom_types` API (fix #6357)
- server: fix issue with event triggers defined on a table which is partitioned (fixes #6261) - server: fix issue with event triggers defined on a table which is partitioned (fixes #6261)
- server: action array relationships now support the same input arguments (such as where or distinct_on) as usual relationships
- server: action array relationships now support aggregate relationships
- server: fix issue with non-optional fields of the remote schema being added as optional in the graphql-engine (fix #6401) - server: fix issue with non-optional fields of the remote schema being added as optional in the graphql-engine (fix #6401)
- server: accept new config `allowed_skew` in JWT config to provide leeway for JWT expiry (fixes #2109) - server: accept new config `allowed_skew` in JWT config to provide leeway for JWT expiry (fixes #2109)
- server: fix issue with query actions with relationship with permissions configured on the remote table (fix #6385) - server: fix issue with query actions with relationship with permissions configured on the remote table (fix #6385)

View File

@ -4,20 +4,21 @@ module Hasura.GraphQL.Schema.Action
, actionAsyncQuery , actionAsyncQuery
) where ) where
import Data.Has
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict as Map
import qualified Language.GraphQL.Draft.Syntax as G import qualified Language.GraphQL.Draft.Syntax as G
import Data.Has
import Data.Text.Extended
import Data.Text.NonEmpty
import qualified Hasura.GraphQL.Parser as P import qualified Hasura.GraphQL.Parser as P
import qualified Hasura.GraphQL.Parser.Internal.Parser as P import qualified Hasura.GraphQL.Parser.Internal.Parser as P
import qualified Hasura.RQL.DML.Internal as RQL import qualified Hasura.RQL.DML.Internal as RQL
import qualified Hasura.RQL.IR.Select as RQL import qualified Hasura.RQL.IR.Select as RQL
import Data.Text.Extended
import Data.Text.NonEmpty
import Hasura.Backends.Postgres.SQL.Types import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.SQL.Value import Hasura.Backends.Postgres.SQL.Value
import Hasura.GraphQL.Parser (FieldParser, InputFieldsParser, Kind (..), import Hasura.GraphQL.Parser (FieldParser, InputFieldsParser, Kind (..),
@ -171,7 +172,7 @@ actionOutputFields annotatedObject = do
scalarOrEnumFields = map scalarOrEnumFieldParser $ toList $ _otdFields outputObject scalarOrEnumFields = map scalarOrEnumFieldParser $ toList $ _otdFields outputObject
relationshipFields <- forM (_otdRelationships outputObject) $ traverse relationshipFieldParser relationshipFields <- forM (_otdRelationships outputObject) $ traverse relationshipFieldParser
let allFieldParsers = scalarOrEnumFields <> let allFieldParsers = scalarOrEnumFields <>
maybe [] (catMaybes . toList) relationshipFields maybe [] (concat . catMaybes . toList) relationshipFields
outputTypeName = unObjectTypeName $ _otdName outputObject outputTypeName = unObjectTypeName $ _otdName outputObject
outputTypeDescription = _otdDescription outputObject outputTypeDescription = _otdDescription outputObject
pure $ P.selectionSet outputTypeName outputTypeDescription allFieldParsers pure $ P.selectionSet outputTypeName outputTypeDescription allFieldParsers
@ -195,26 +196,37 @@ actionOutputFields annotatedObject = do
relationshipFieldParser relationshipFieldParser
:: TypeRelationship (TableInfo 'Postgres) (ColumnInfo 'Postgres) :: TypeRelationship (TableInfo 'Postgres) (ColumnInfo 'Postgres)
-> m (Maybe (FieldParser n (RQL.AnnFieldG 'Postgres (UnpreparedValue 'Postgres)))) -> m (Maybe [FieldParser n (RQL.AnnFieldG 'Postgres (UnpreparedValue 'Postgres))])
relationshipFieldParser typeRelationship = runMaybeT do relationshipFieldParser (TypeRelationship relName relType _ tableInfo fieldMapping) = runMaybeT do
let TypeRelationship relName relType _ tableInfo fieldMapping = typeRelationship let tableName = _tciName $ _tiCoreInfo tableInfo
tableName = _tciName $ _tiCoreInfo tableInfo fieldName = unRelationshipName relName
fieldName = unRelationshipName relName tableRelName = RelName $ mkNonEmptyTextUnsafe $ G.unName fieldName
roleName <- lift askRoleName columnMapping = Map.fromList $ do
(k, v) <- Map.toList fieldMapping
pure (unsafePGCol $ G.unName $ unObjectFieldName k, pgiColumn v)
roleName <- lift askRoleName
tablePerms <- MaybeT $ pure $ RQL.getPermInfoMaybe roleName PASelect tableInfo tablePerms <- MaybeT $ pure $ RQL.getPermInfoMaybe roleName PASelect tableInfo
tableParser <- lift $ selectTable tableName fieldName Nothing tablePerms case relType of
pure $ tableParser <&> \selectExp -> ObjRel -> do
let tableRelName = RelName $ mkNonEmptyTextUnsafe $ G.unName fieldName let desc = Just $ G.Description "An object relationship"
columnMapping = Map.fromList $ selectionSetParser <- lift $ tableSelectionSet tableName tablePerms
[ (unsafePGCol $ G.unName $ unObjectFieldName k, pgiColumn v) pure $ pure $ P.nonNullableField $
| (k, v) <- Map.toList fieldMapping P.subselection_ fieldName desc selectionSetParser
] <&> \fields -> RQL.AFObjectRelation $ RQL.AnnRelationSelectG tableRelName columnMapping $
in case relType of RQL.AnnObjectSelectG fields tableName $
ObjRel -> RQL.AFObjectRelation $ RQL.AnnRelationSelectG tableRelName columnMapping $ fmapAnnBoolExp partialSQLExpToUnpreparedValue $ spiFilter tablePerms
RQL.AnnObjectSelectG (RQL._asnFields selectExp) tableName $ ArrRel -> do
RQL._tpFilter $ RQL._asnPerm selectExp let desc = Just $ G.Description "An array relationship"
ArrRel -> RQL.AFArrayRelation $ RQL.ASSimple $ otherTableParser <- lift $ selectTable tableName fieldName desc tablePerms
RQL.AnnRelationSelectG tableRelName columnMapping selectExp let arrayRelField = otherTableParser <&> \selectExp -> RQL.AFArrayRelation $
RQL.ASSimple $ RQL.AnnRelationSelectG tableRelName columnMapping selectExp
relAggFieldName = fieldName <> $$(G.litName "_aggregate")
relAggDesc = Just $ G.Description "An aggregate relationship"
tableAggField <- lift $ selectTableAggregate tableName relAggFieldName relAggDesc tablePerms
pure $ catMaybes [ Just arrayRelField
, fmap (RQL.AFArrayRelation . RQL.ASAggregate . RQL.AnnRelationSelectG tableRelName columnMapping) <$> tableAggField
]
mkDefinitionList :: AnnotatedObjectType 'Postgres -> [(PGCol, ScalarType 'Postgres)] mkDefinitionList :: AnnotatedObjectType 'Postgres -> [(PGCol, ScalarType 'Postgres)]
mkDefinitionList AnnotatedObjectType{..} = mkDefinitionList AnnotatedObjectType{..} =

View File

@ -0,0 +1,125 @@
- description: Run create_user sync action mutation with valid email
url: /v1/graphql
status: 200
query:
query: |
mutation {
create_user(email: "clarke@gmail.com", name: "Clarke"){
__typename
id
user {
__typename
name
email
is_admin
}
}
}
response:
data:
create_user:
__typename: UserId
id: 1
user:
__typename: user
name: Clarke
email: clarke@gmail.com
is_admin: false
- description: Use user_by_email to get our user and test array relationship
url: /v1/graphql
status: 200
query:
query: |
query {
get_user_by_email(email: "clarke@gmail.com"){
__typename
id
articles {
name
}
}
}
response:
data:
get_user_by_email:
__typename: UserId
id: 1
articles:
- name: foo
- name: bar
- name: bar
- description: Use user_by_email to get our user and test array relationship with limit input parameter
url: /v1/graphql
status: 200
query:
query: |
query {
get_user_by_email(email: "clarke@gmail.com"){
__typename
id
articles(limit: 1) {
name
}
}
}
response:
data:
get_user_by_email:
__typename: UserId
id: 1
articles:
- name: foo
- description: Use user_by_email to get our user and test array relationship with distinct input parameter
url: /v1/graphql
status: 200
query:
query: |
query {
get_user_by_email(email: "clarke@gmail.com"){
__typename
id
articles(distinct_on: name) {
name
}
}
}
response:
data:
get_user_by_email:
__typename: UserId
id: 1
articles:
- name: bar
- name: foo
- description: Use user_by_email to get our user and test aggregate array relationship
url: /v1/graphql
status: 200
query:
query: |
query {
get_user_by_email(email: "clarke@gmail.com"){
__typename
id
articles_aggregate {
aggregate {
max {
name
}
}
}
}
}
response:
data:
get_user_by_email:
__typename: UserId
id: 1
articles:
- name: bar
- name: foo

View File

@ -0,0 +1,48 @@
- description: Run create_user sync action mutation with valid email
url: /v1/graphql
status: 200
query:
query: |
mutation {
create_user(email: "clarke@gmail.com", name: "Clarke"){
__typename
id
user {
__typename
name
email
is_admin
}
}
}
response:
data:
create_user:
__typename: UserId
id: 1
user:
__typename: user
name: Clarke
email: clarke@gmail.com
is_admin: false
- description: Use user_by_email to get our user and test object relationship
url: /v1/graphql
status: 200
query:
query: |
query {
get_user_by_email(email: "clarke@gmail.com"){
__typename
id
user(limit: 4) {
name
}
}
}
response:
errors:
- extensions:
path: $.selectionSet.get_user_by_email.selectionSet.user
code: validation-failed
message: '"user" has no argument named "limit"'

View File

@ -10,12 +10,27 @@ args:
email TEXT NOT NULL, email TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT false is_admin BOOLEAN NOT NULL DEFAULT false
); );
CREATE TABLE "article"(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
user_id INTEGER
);
INSERT INTO "article" (name, user_id) VALUES
('foo', 1),
('bar', 1),
('bar', 1),
('baz', 2);
- type: track_table - type: track_table
args: args:
name: user name: user
schema: public schema: public
- type: track_table
args:
name: article
schema: public
- type: set_custom_types - type: set_custom_types
args: args:
input_objects: input_objects:
@ -46,6 +61,11 @@ args:
remote_table: user remote_table: user
field_mapping: field_mapping:
id: id id: id
- name: articles
type: array
remote_table: article
field_mapping:
id: user_id
- name: OutObject - name: OutObject
fields: fields:

View File

@ -29,3 +29,4 @@ args:
cascade: true cascade: true
sql: | sql: |
DROP TABLE "user"; DROP TABLE "user";
DROP TABLE "article";

View File

@ -46,6 +46,12 @@ class TestActionsSyncWebsocket:
def test_create_user_success(self, hge_ctx, transport): def test_create_user_success(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_success.yaml', transport) check_query_f(hge_ctx, self.dir() + '/create_user_success.yaml', transport)
def test_create_user_relationship(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_relationship.yaml', transport)
def test_create_user_relationship(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_user_relationship_fail.yaml', transport)
def test_create_users_fail(self, hge_ctx, transport): def test_create_users_fail(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/create_users_fail.yaml', transport) check_query_f(hge_ctx, self.dir() + '/create_users_fail.yaml', transport)