Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x]: GraphQL type name conflict when overriding Matrix field handle #16020

Open
yoannisj opened this issue Nov 5, 2024 · 6 comments
Open
Labels

Comments

@yoannisj
Copy link

yoannisj commented Nov 5, 2024

What happened?

Description

If two entry types use two different matrix fields, which support different entry-types, but use the same handle for these matrix fields, GraphQL throws an error indicating the nested entry types are not available for the matrix field.

It seems like the Matrix field uses the overridden handle to generate it's GraphQL content type name, and that creates a conflict meaning that the schema is missing some types.

Steps to reproduce

I have quickly setup a small Craft-CMS project repository which you can clone, with a simple project config that reproduces the error (steps 1 to 8 below). To recreate the issue in another Craft-CMS project, setup the following project config:

  1. Create an entry type named "Text Block" (textBlock)
  2. Create an entry type named "Image Block" (imageBlock)
  3. Create an entry type named "Gallery Block" (galleryBlock)
  4. Create a Matrix field named "Simple Body Blocks" (simpleBodyBlocks) with support for the following nested entry-types:
    • "Text Block" (textBlock)
    • "Image Block" (imageBlock)
  5. Create an entry type named "Simple Page" and add the "Simple Body Blocks" Matrix field to its field layout with the overridden field handle bodyBlocks.
  6. Create a Matrix field named "Complex Body Blocks" (complexBodyBlocks) with support for the following nested entry-types:
    • "Text Block" (textBlock)
    • "Image Block"(imageBlock)
    • "Gallery Block"(galleryBlock)
  7. Create an entry type named "Complex Page" and add the "Complex Body Blocks" Matrix field to its field layout, with the overridden field handle bodyBlocks.
  8. Create a section named "Pages" and add the "Simple Page" and "Complex Page" entry-types to it (this is only so we can easily create the data needed to reproduce the bug, and does not seem relevant for causing the bug itself).

Once you have the project config setup:

  1. Create 1x "Simple Page" entry and add 1x "Text Block" and 1x "Image Block" to its bodyBlocks field.
  2. Create 1x "Complex Page" entry and add 1x "Text Block", 1x "Image Block" and 1x "Gallery Block" to its bodyBlocks field.
  3. Open GraphiQL and run the following query:
query {
  pagesEntries {
    ... on simplePage_Entry {  
      bodyBlocks {
        __typename
        ... on textBlock_Entry {
          id
          typeHandle
          title
        }
        ... on imageBlock_Entry {
          id
          typeHandle
          title
        }
      }
    }
    ... on complexPage_Entry {
      bodyBlocks {
        __typename
        ... on textBlock_Entry {
          id
          typeHandle
          title
        }
        ... on imageBlock_Entry {
          id
          typeHandle
          title
        }
        ... on galleryBlock_Entry {
          id
          typeHandle
          title
        }
      }
    }
  }
}

Expected behavior

The GraphQL API should return the data for the two entries and the nested entries created in their matrix field (steps 9-11):

See Data
{
  "data": {
    "pagesEntries": [
      {
        "bodyBlocks": [
          {
            "__typename": "textBlock_Entry",
            "id": "21",
            "typeHandle": "textBlock",
            "title": "Test Text (Simple)"
          },
          {
            "__typename": "imageBlock_Entry",
            "id": "22",
            "typeHandle": "imageBlock",
            "title": "Test Image (Simple)"
          }
        ]
      },
      {
        "bodyBlocks": [
          {
            "__typename": "imageBlock_Entry",
            "id": "27",
            "typeHandle": "imageBlock",
            "title": "Test Image (Complex)"
          },
          {
            "__typename": "textBlock_Entry",
            "id": "28",
            "typeHandle": "textBlock",
            "title": "Test Text (Complex)"
          },
          {
            "__typename": "galleryBlock_Entry",
            "id": "29",
            "typeHandle": "galleryBlock",
            "title": "Test Gallery (Complex)"
          }
        ]
      }
    ]
  }
}

Actual behavior

The GraphQL API returns the following error data:

Fragment cannot be spread here as objects of type \"bodyBlocks_MatrixField\" can never be of type \"galleryBlock_Entry\".
See Data ```json { "errors": [ { "message": "Fragment cannot be spread here as objects of type \"bodyBlocks_MatrixField\" can never be of type \"galleryBlock_Entry\".", "extensions": { "category": "graphql" }, "locations": [ { "line": 32, "column": 9 } ] } ] } ```

If we remove the ... on galleryBlock_Entry {} fragment from the GraphQL query, another error is returned and the galleryBlock entry is missing from the data:

Runtime Object type \"galleryBlock_Entry\" is not a possible type for \"bodyBlocks_MatrixField\"."
See Data ```json { "errors": [ { "debugMessage": "Runtime Object type \"galleryBlock_Entry\" is not a possible type for \"bodyBlocks_MatrixField\".", "message": "Internal server error", "extensions": { "category": "internal" }, "trace": [ { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 974, "call": "GraphQL\\Executor\\ReferenceExecutor::ensureValidRuntimeType('galleryBlock_Entry', GraphQLType: bodyBlocks_MatrixField, instance of GraphQL\\Type\\Definition\\ResolveInfo, instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 789, "call": "GraphQL\\Executor\\ReferenceExecutor::completeAbstractValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 887, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 761, "call": "GraphQL\\Executor\\ReferenceExecutor::completeListValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 740, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 556, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1195, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: complexPage_Entry, instance of craft\\elements\\Entry, instance of ArrayObject(1), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1145, "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: complexPage_Entry, instance of craft\\elements\\Entry, array(2), instance of ArrayObject(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1105, "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: complexPage_Entry, instance of ArrayObject(1), array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 973, "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: complexPage_Entry, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 789, "call": "GraphQL\\Executor\\ReferenceExecutor::completeAbstractValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 887, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 761, "call": "GraphQL\\Executor\\ReferenceExecutor::completeListValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 556, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1195, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: Query, null, instance of ArrayObject(1), array(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 264, "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: Query, null, array(0), instance of ArrayObject(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 215, "call": "GraphQL\\Executor\\ReferenceExecutor::executeOperation(instance of GraphQL\\Language\\AST\\OperationDefinitionNode, null)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/Executor.php", "line": 156, "call": "GraphQL\\Executor\\ReferenceExecutor::doExecute()" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 161, "call": "GraphQL\\Executor\\Executor::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, instance of GraphQL\\Language\\AST\\DocumentNode, null, array(2), null, null, null)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 93, "call": "GraphQL\\GraphQL::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, array(2), null, null, null, array(26))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/services/Gql.php", "line": 526, "call": "GraphQL\\GraphQL::executeQuery(instance of GraphQL\\Type\\Schema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, array(2), null, null, null, array(26))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/controllers/GraphqlController.php", "line": 195, "call": "craft\\services\\Gql::executeQuery(instance of craft\\models\\GqlSchema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, null, true)" }, { "call": "craft\\controllers\\GraphqlController::actionApi()" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/InlineAction.php", "line": 57, "function": "call_user_func_array(array(2), array(0))" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Controller.php", "line": 178, "call": "yii\\base\\InlineAction::runWithParams(array(1))" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Module.php", "line": 552, "call": "yii\\base\\Controller::runAction('api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 350, "call": "yii\\base\\Module::runAction('graphql/api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 649, "call": "craft\\web\\Application::runAction('graphql/api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 312, "call": "craft\\web\\Application::_processActionRequest(instance of craft\\web\\Request)" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Application.php", "line": 384, "call": "craft\\web\\Application::handleRequest(instance of craft\\web\\Request)" }, { "file": "/var/www/html/web/index.php", "line": 12, "call": "yii\\base\\Application::run()" } ] } ], "data": { "pagesEntries": [ { "bodyBlocks": [ { "__typename": "textBlock_Entry", "id": "21", "typeHandle": "textBlock", "title": "Test Text (Simple)" }, { "__typename": "imageBlock_Entry", "id": "22", "typeHandle": "imageBlock", "title": "Test Image (Simple)" } ] }, { "bodyBlocks": [ { "__typename": "imageBlock_Entry", "id": "27", "typeHandle": "imageBlock", "title": "Test Image (Complex)" }, { "__typename": "textBlock_Entry", "id": "28", "typeHandle": "textBlock", "title": "Test Text (Complex)" }, null ] } ] } } ```

Suggested Solution

We suspect the problem is that the Matrix field generates a type name based on its handle in the current field layout (i.e. bodyBlocks in the example setup above). This creates a conflict, and as a result only one of the two Matrix types exist in the GraphQL schema. This could be solved by using the original handle (the handle defined in the CP > Settings > Fields section) in the field's getContentGqlType() method to avoid such conflicts.

We could not try out this solution because –at the time when the getContentGqlType() method is called– the Matrix Field's handle property is set to the overridden handle, and there is no originalHandle property. We quickly tested including the field's id in the type name, and –although we suspect that would introduce new problems– it did solve the issue.

Craft CMS version

5.4.9

PHP version

8.2.22

Operating system and version

Linux 6.10.12-orbstack-00282-gd1783374c25e

Database type and version

MySQL 8.0.36

Image driver and version

Imagick 3.7.0 (ImageMagick 6.9.11-60)

Installed plugins and versions

(None)

@yoannisj yoannisj added the bug label Nov 5, 2024
@yoannisj
Copy link
Author

yoannisj commented Nov 5, 2024

I am not sure if it's the same issue, but #15708 is reporting a similar error message.

@denisyilmaz
Copy link

@brandonkelly as this issue is blocking us going forward with a current project it would be great to hear if this problem is something that can be addressed or if we should apply workaround (ex. not to use same handles for different matrix fields).

@brandonkelly
Copy link
Member

@denisyilmaz Yeah there have always been some gotchas with GraphQL when multiple fields have the same handle. Your best bet is to choose unique names.

@denisyilmaz
Copy link

@brandonkelly thanks for the info. So this will not be addressed any time soon?

What about the suggested solution we provided:

We could not try out this solution because –at the time when the getContentGqlType() method is called– the Matrix Field's handle property is set to the overridden handle, and there is no originalHandle property. We quickly tested including the field's id in the type name, and –although we suspect that would introduce new problems– it did solve the issue.

@brandonkelly
Copy link
Member

Field IDs will change from environment to environment, and can’t change the GraphQL names without breaking every existing GraphQL implementation.

You can get the original handle via $field->layoutElement?->getOriginalHandle() though.

@denisyilmaz
Copy link

denisyilmaz commented Nov 8, 2024

@brandonkelly Thanks, while this is actually helping in not throwing an error, it still does not return the corresponding field values:

    /**
     * @inheritdoc
     * @since 3.3.0
     */
    public function getContentGqlType(): Type|array
    {
        $typeArray = EntryTypeGenerator::generateTypes($this);
        $typeName = $this->layoutElement?->getOriginalHandle() . '_MatrixField';

        return [
            'name' => $this->handle,
            'type' => Type::nonNull(Type::listOf(Gql::getUnionType($typeName, $typeArray))),
            'args' => EntryArguments::getArguments(),
            'resolve' => EntryResolver::class . '::resolve',
            'complexity' => Gql::eagerLoadComplexity(),
        ];
    }

in craftcms/cms/src/fields/Matrix.php

so we will not use overlapping field handles for now. Still, this feature (renaming field names) is great and we would love to see it working globally with GraphQL.. Hope this is possible somewhere in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants