Repository: edgedb/edgedb

Issue:
Backward links in views may break GraphQL
The following is legal view and should not break GraphQL:
```
view X := User {
   # this will make a computable link
   # pointing to std::Object
   owned := User.<owner
}
```


Discussion:

Resolving patch:
diff --git a/edb/graphql/types.py b/edb/graphql/types.py
--- a/edb/graphql/types.py
+++ b/edb/graphql/types.py
@@ -280,10 +280,17 @@ def _get_query_args(self, typename):
         }
 
     def _get_insert_args(self, typename):
+        # The data can only be a specific non-interface type, if no
+        # such type exists, skip it as we cannot accept unambiguous
+        # data input. It's still possible to just select some existing
+        # data.
+        intype = self._gql_inobjtypes.get(f'Insert{typename}')
+        if intype is None:
+            return {}
+
         return {
             'data': GraphQLArgument(
-                GraphQLNonNull(GraphQLList(GraphQLNonNull(
-                    self._gql_inobjtypes[f'Insert{typename}'])))),
+                GraphQLNonNull(GraphQLList(GraphQLNonNull(intype)))),
         }
 
     def _get_update_args(self, typename):
@@ -319,10 +326,12 @@ def get_fields(self, typename):
                     GraphQLList(GraphQLNonNull(gqltype)),
                     args=self._get_query_args(name),
                 )
-                fields[f'insert_{gname}'] = GraphQLField(
-                    GraphQLList(GraphQLNonNull(gqltype)),
-                    args=self._get_insert_args(name),
-                )
+                args = self._get_insert_args(name)
+                if args:
+                    fields[f'insert_{gname}'] = GraphQLField(
+                        GraphQLList(GraphQLNonNull(gqltype)),
+                        args=args,
+                    )
 
             for name, gqltype in sorted(self._gql_interfaces.items(),
                                         key=lambda x: x[1].name):
@@ -609,23 +618,31 @@ def _make_generic_nested_update_type(self, edb_base):
     def _make_generic_nested_insert_type(self, edb_base):
         typename = edb_base.get_name(self.edb_schema)
         name = f'NestedInsert{typename}'
+        fields = {
+            'filter': GraphQLInputObjectField(
+                self._gql_inobjtypes[typename]),
+            'order': GraphQLInputObjectField(
+                self._gql_ordertypes[typename]),
+            'first': GraphQLInputObjectField(GraphQLInt),
+            'last': GraphQLInputObjectField(GraphQLInt),
+            # before and after are supposed to be opaque values
+            # serialized to string
+            'before': GraphQLInputObjectField(GraphQLString),
+            'after': GraphQLInputObjectField(GraphQLString),
+        }
+
+        # The data can only be a specific non-interface type, if no
+        # such type exists, skip it as we cannot accept unambiguous
+        # data input. It's still possible to just select some existing
+        # data.
+        data_t = self._gql_inobjtypes.get(f'Insert{typename}')
+        if data_t:
+            fields['data'] = GraphQLInputObjectField(data_t)
+
         nitype = GraphQLInputObjectType(
             name=self.get_input_name(
                 'NestedInsert', self.get_gql_name(typename)),
-            fields={
-                'data': GraphQLInputObjectField(
-                    self._gql_inobjtypes[f'Insert{typename}']),
-                'filter': GraphQLInputObjectField(
-                    self._gql_inobjtypes[typename]),
-                'order': GraphQLInputObjectField(
-                    self._gql_ordertypes[typename]),
-                'first': GraphQLInputObjectField(GraphQLInt),
-                'last': GraphQLInputObjectField(GraphQLInt),
-                # before and after are supposed to be opaque values
-                # serialized to string
-                'before': GraphQLInputObjectField(GraphQLString),
-                'after': GraphQLInputObjectField(GraphQLString),
-            },
+            fields=fields,
         )
 
         self._gql_inobjtypes[name] = nitype
@@ -821,11 +838,12 @@ def _define_types(self):
 
             # input object types corresponding to this object (only
             # real objects can appear as input objects)
-            gqlinserttype = GraphQLInputObjectType(
-                name=self.get_input_name('Insert', gql_name),
-                fields=partial(self.get_insert_fields, t_name),
-            )
-            self._gql_inobjtypes[f'Insert{t_name}'] = gqlinserttype
+            if not t.is_view(self.edb_schema):
+                gqlinserttype = GraphQLInputObjectType(
+                    name=self.get_input_name('Insert', gql_name),
+                    fields=partial(self.get_insert_fields, t_name),
+                )
+                self._gql_inobjtypes[f'Insert{t_name}'] = gqlinserttype
 
     def get(self, name, *, dummy=False):
         '''Get a special GQL type either by name or based on EdgeDB type.'''


Test patch:
diff --git a/tests/schemas/graphql.esdl b/tests/schemas/graphql.esdl
--- a/tests/schemas/graphql.esdl
+++ b/tests/schemas/graphql.esdl
@@ -43,6 +43,8 @@ type User extending NamedObject {
     required property age -> int64;
     required property score -> float64;
     link profile -> Profile;
+    # a link pointing to an abstract type
+    multi link favorites -> NamedObject;
 }
 
 alias SettingAlias := Setting {
@@ -55,6 +57,12 @@ alias SettingAliasAugmented := Setting {
     }
 };
 
+alias ProfileAlias := Profile {
+    # although this will point to an actual user, but the type system
+    # will only resolve an Object here
+    owner := .<profile
+};
+
 type Person extending User;
 
 scalar type positive_int_t extending int64 {
diff --git a/tests/schemas/graphql_setup.edgeql b/tests/schemas/graphql_setup.edgeql
--- a/tests/schemas/graphql_setup.edgeql
+++ b/tests/schemas/graphql_setup.edgeql
@@ -61,7 +61,8 @@ INSERT User {
         name := 'Alice profile',
         value := 'special',
         tags := ['1st', '2nd'],
-    })
+    }),
+    favorites := {Setting, UserGroup}
 };
 
 INSERT Person {
diff --git a/tests/test_http_graphql_mutation.py b/tests/test_http_graphql_mutation.py
--- a/tests/test_http_graphql_mutation.py
+++ b/tests/test_http_graphql_mutation.py
@@ -20,6 +20,8 @@
 import os
 import unittest  # NOQA
 
+import edgedb
+
 from edb.testbase import http as tb
 
 
@@ -989,6 +991,55 @@ def test_graphql_mutation_insert_nested_07(self):
             "delete_UserGroup": data['groups']
         })
 
+    def test_graphql_mutation_insert_bad_01(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'Cannot query field "insert_SettingAlias"'):
+            self.graphql_query(r"""
+                mutation insert_SettingAlias {
+                    insert_SettingAlias(
+                        data: [{
+                            name: "badsetting01",
+                            value: "red"
+                        }]
+                    ) {
+                        name
+                        value
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_insert_bad_02(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'Argument "data" has invalid value(.|\n)*'
+                r'In field "favorites": .*In field "data": Unknown field'):
+            self.graphql_query(r"""
+                mutation insert_User {
+                    insert_User(
+                        data: [{
+                            name: "Bad User02",
+                            active: true,
+                            age: 33,
+                            score: 33.33,
+                            favorites: [{
+                                data: {
+                                    name: "badsetting02",
+                                }
+                            }],
+                        }]
+                    ) {
+                        name
+                        age
+                        score
+                        favorites {
+                            name
+                            value
+                        }
+                    }
+                }
+            """)
+
     def test_graphql_mutation_update_scalars_01(self):
         orig_data = {
             'p_bool': True,
@@ -2015,3 +2066,74 @@ def test_graphql_mutation_update_link_03(self):
                 }],
             }]
         })
+
+    def test_graphql_mutation_update_bad_01(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'Cannot query field "update_SettingAlias"'):
+            self.graphql_query(r"""
+                mutation update_SettingAlias {
+                    update_SettingAlias(
+                        filter: {name: {eq: "template"}}
+                        data: {
+                            value: {set: "red"},
+                        }
+                    ) {
+                        name
+                        value
+                    }
+                }
+            """)
+
+    def test_graphql_mutation_delete_alias_01(self):
+        self.assert_graphql_query_result(r"""
+            mutation insert_Setting {
+                insert_Setting(
+                    data: [{
+                        name: "delsetting01",
+                        value: "red"
+                    }]
+                ) {
+                    name
+                    value
+                }
+            }
+        """, {
+            "insert_Setting": [{
+                'name': 'delsetting01',
+                'value': 'red',
+            }]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation delete_SettingAlias {
+                delete_SettingAlias(
+                    filter: {
+                        name: {eq: "delsetting01"}
+                    }
+                ) {
+                    name
+                    value
+                }
+            }
+        """, {
+            "delete_SettingAlias": [{
+                'name': 'delsetting01',
+                'value': 'red',
+            }]
+        })
+
+        self.assert_graphql_query_result(r"""
+            query get_SettingAlias {
+                SettingAlias(
+                    filter: {
+                        name: {eq: "delsetting01"}
+                    }
+                ) {
+                    name
+                    value
+                }
+            }
+        """, {
+            "SettingAlias": []
+        })
diff --git a/tests/test_http_graphql_query.py b/tests/test_http_graphql_query.py
--- a/tests/test_http_graphql_query.py
+++ b/tests/test_http_graphql_query.py
@@ -522,6 +522,29 @@ def test_graphql_functional_query_17(self):
             variables={'name': None},
         )
 
+    def test_graphql_functional_query_18(self):
+        # test filtering by nested object
+        self.assert_graphql_query_result(r"""
+            query {
+                User(filter: {name: {eq: "Alice"}}) {
+                    name
+                    favorites(order: {name: {dir: ASC}}) {
+                        name
+                    }
+                }
+            }
+        """, {
+            'User': [{
+                'name': 'Alice',
+                'favorites': [
+                    {'name': 'basic'},
+                    {'name': 'perks'},
+                    {'name': 'template'},
+                    {'name': 'upgraded'},
+                ]
+            }],
+        })
+
     def test_graphql_functional_alias_01(self):
         self.assert_graphql_query_result(
             r"""
@@ -652,6 +675,59 @@ def test_graphql_functional_alias_03(self):
             sort=lambda x: x['name']
         )
 
+    def test_graphql_functional_alias_04(self):
+        self.assert_graphql_query_result(
+            r"""
+                {
+                    ProfileAlias {
+                        __typename
+                        name
+                        value
+                        owner {
+                            __typename
+                            id
+                        }
+                    }
+                }
+            """,
+            {
+                "ProfileAlias": [
+                    {
+                        "__typename": "ProfileAliasType",
+                        "name": "Alice profile",
+                        "value": "special",
+                        "owner": [
+                            {
+                                "__typename": "UserType",
+                                "id": uuid.UUID,
+                            }
+                        ]
+                    }
+                ]
+            },
+        )
+
+        result = self.graphql_query(r"""
+            query {
+                ProfileAlias {
+                    owner {
+                        id
+                    }
+                }
+            }
+        """)
+        user_id = result['ProfileAlias'][0]['owner'][0]['id']
+
+        self.assert_graphql_query_result(f"""
+            query {{
+                User(filter: {{id: {{eq: "{user_id}"}}}}) {{
+                    name
+                }}
+            }}
+        """, {
+            'User': [{'name': 'Alice'}]
+        })
+
     def test_graphql_functional_arguments_01(self):
         result = self.graphql_query(r"""
             query {
@@ -666,7 +742,7 @@ def test_graphql_functional_arguments_01(self):
         alice = [res for res in result['User']
                  if res['name'] == 'Alice'][0]
 
-        result = self.assert_graphql_query_result(f"""
+        self.assert_graphql_query_result(f"""
             query {{
                 User(filter: {{id: {{eq: "{alice['id']}"}}}}) {{
                     id