craftcms / cms

Build bespoke content experiences with Craft.
https://craftcms.com
Other
3.28k stars 635 forks source link

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

Open yoannisj opened 2 hours ago

yoannisj commented 2 hours ago

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 ```json { "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 commented 2 hours ago

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