Skip to content

Prefetching nested fields with shared Type instances throws error #729

Open
@cdomigan

Description

@cdomigan

Thank you for this wonderful library.

Until the change introduced by #702 by @sudevva I was able to "share" instances of Types in my prefetch logic.

The change to this line:

$prefetchBuffer->purgeResult($source);

means that the source is now purged from results. If I then want to return the same Type instance later, I will get an error:

UnexpectedValueException: Object not found
/workspaces/graphqlite/src/PrefetchBuffer.php:90

This error only shows up in a nested prefetch context, specifically where an intermediate type is being prefetched, instances of that type are being shared, and it has a prefetched child field also.

In the test suite on this repo, this demonstrates the issue:

// tests/Integration/EndToEndTest.php

// A NEW TEST TO DEMONSTRATE THE ISSUE
    public function testPrefetchingDeeplyNested(): void
    {
        $schema = $this->mainContainer->get(Schema::class);
        assert($schema instanceof Schema);

        $schema->assertValid();

        $queryString = '
        query {
            companies {
                contact {
                    name
                    supervisor {
                        name
                    }
                }
            }
        }
        ';

        $result = GraphQL::executeQuery(
            $schema,
            $queryString,
            null,
            new Context(),
        );

        $this->assertSame([
            'companies' => [
                [
                    'contact' => [
                        'name' => 'Kate',
                        'supervisor' => [
                            'name' => 'The Boss',
                        ],
                    ],
                ],
                [
                    'contact' => [
                        'name' => 'Kate',
                        'supervisor' => [
                            'name' => 'The Boss',
                        ],
                    ],
                ],
            ]
        ], $this->getSuccessResult($result));
    }
// tests/Fixtures/Integration/Controllers/CompanyController
/**
     * @return Company[]
     */
    #[Query]
    public function getCompanies(): array
    {
        return [new Company('Company1'), new Company('Company2')];
    }
// tests/Fixtures/Integration/Types/CompanyType.php
    /**
     * @param Company[] $companies
     *
     * @return Contact[]
     */
    public static function prefetchContacts(array $companies): array
    {
        $contacts = [];

        $kate = new Contact('Kate');

        foreach ($companies as $company) {
            // This works
            //$contacts[$company->name] = new Contact('Kate');

            // This does not work
            $contacts[$company->name] = $kate;
        }

        return $contacts;
    }
// tests/Fixtures/Integration/Models/Contact.php
    /** @param Contact[] $contacts */
    #[Field]
    public function getSupervisor(#[Prefetch('prefetchSupervisors')] array $supervisors): Contact|null
    {
        return $supervisors[$this->name] ?? null;
    }

    /**
     * @param Contact[] $contacts
     *
     * @return Contact[]
     */
    public static function prefetchSupervisors(array $contacts): array
    {
        $supervisors = [];

        $theBoss = new Contact('The Boss');

        foreach ($contacts as $contacts) {
            $supervisors[$contacts->name] = $theBoss;
        }

        return $supervisors;
    }

Notably, it is the sharing of the $kate Type instance in the prefetchContacts method on CompanyType.php that causes the error:

There was 1 error:

1) TheCodingMachine\GraphQLite\Integration\EndToEndTest::testPrefetchingDeeplyNested
UnexpectedValueException: Object not found

/workspaces/graphqlite/src/PrefetchBuffer.php:90
/workspaces/graphqlite/src/Parameters/PrefetchDataParameter.php:60
/workspaces/graphqlite/vendor/webonyx/graphql-php/src/Executor/Promise/Adapter/SyncPromise.php:63
/workspaces/graphqlite/vendor/webonyx/graphql-php/src/Executor/Promise/Adapter/SyncPromise.php:50
/workspaces/graphqlite/vendor/webonyx/graphql-php/src/Executor/Promise/Adapter/SyncPromiseAdapter.php:147
/workspaces/graphqlite/vendor/webonyx/graphql-php/src/GraphQL.php:109
/workspaces/graphqlite/tests/Integration/EndToEndTest.php:2477

If I revert to using a new Type instance then the error goes away (see comment in above code). But this means I can't cache/reuse my Type instances in deeply nested prefetch scenarios.

Note if I don't fetch the child field supervisor (itself prefetched) then the error goes away. It is only apparent with nested prefetches.

Commenting out this line appears to fix the issue, but I'm unsure if this has repercussions elsewhere:

$prefetchBuffer->purgeResult($source);

Is this expected behaviour? Am I meant to always return a new Type instance for each result, even if the result is identical and used elsewhere? This seems like a change if so?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions