diff --git a/src/Schema/DataObject/FieldAccessor.php b/src/Schema/DataObject/FieldAccessor.php index 402aad7cc..37244151a 100644 --- a/src/Schema/DataObject/FieldAccessor.php +++ b/src/Schema/DataObject/FieldAccessor.php @@ -238,7 +238,12 @@ private function parsePath($subject, array $path) $aggregateFunction )); } - return call_user_func_array([$subject, $aggregateFunction], [$aggregateColumn]); + if (method_exists($subject, $aggregateFunction)) { + return call_user_func_array([$subject, $aggregateFunction], [$aggregateColumn]); + } + + return null; + } $singleton = DataObject::singleton($subject->dataClass()); diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index 3d4791ec9..5852bd1af 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -608,6 +608,7 @@ public function save(): void $this->addType($modelType); } + $this->types[self::QUERY_TYPE] = $this->queryType; if ($this->mutationType->exists()) { $this->types[self::MUTATION_TYPE] = $this->mutationType; diff --git a/tests/Fake/IntegrationTestResolver.php b/tests/Fake/IntegrationTestResolver.php index 51021a68b..b6f329737 100644 --- a/tests/Fake/IntegrationTestResolver.php +++ b/tests/Fake/IntegrationTestResolver.php @@ -2,16 +2,28 @@ namespace SilverStripe\GraphQL\Tests\Fake; +use SilverStripe\GraphQL\Schema\Plugin\PaginationPlugin; + class IntegrationTestResolver { public static function resolveReadMyTypes($obj, $args = []) { return [ - ['field1' => 'foo', 'field2' => 2], - ['field1' => 'bar', 'field2' => 3], + ['field1' => 'foo', 'field2' => 2, 'field3' => 3], + ['field1' => 'bar', 'field2' => 3, 'field3' => 3], ]; } + public static function resolveMyTypeField3($obj, $args = []) + { + $arg = $args['MyArg'] ?? null; + if ($arg) { + return 'arg'; + } + + return 'no arg'; + } + public static function resolveReadMyTypesAgain() { return [ @@ -20,4 +32,24 @@ public static function resolveReadMyTypesAgain() ]; } + public static function lotsOfMyTypes($obj, $args = []) + { + return array_map(function ($num) { + return ['field1' => 'field1-' . $num]; + }, range(1, 100)); + } + + public static function testPaginate($maxLimit) + { + return function ($list, $args) use ($maxLimit) { + $limit = min(($args['limit'] ?? 10), $maxLimit); + $offset = $args['offset'] ?? 0; + return PaginationPlugin::createPaginationResult( + count($list), + array_slice($list, $offset, $limit), + $limit, + $offset + ); + }; + } } diff --git a/tests/Schema/IntegrationTest.php b/tests/Schema/IntegrationTest.php index 99fc549ac..070c8fa7e 100644 --- a/tests/Schema/IntegrationTest.php +++ b/tests/Schema/IntegrationTest.php @@ -40,14 +40,42 @@ protected function tearDownOnce() $this->clean(); } + protected function tearDown() + { + parent::tearDown(); + DataObjectFake::get()->removeAll(); + File::get()->removeAll(); + Member::get()->removeAll(); + } + public function testSimpleType() { - $schema = $this->createSchema(['_test2'], [IntegrationTestResolver::class]); + $schema = $this->createSchema(['_' . __FUNCTION__], [IntegrationTestResolver::class]); + $query = <<querySchema($schema, $query); + $this->assertSuccess($result); + $records = $result['data']['readMyTypes'] ?? []; + $this->assertCount(2, $records); + $this->assertResults([ + ['field1' => 'foo', 'field2' => 2, 'field3' => 'no arg'], + ['field1' => 'bar', 'field2' => 3, 'field3' => 'no arg'], + ], $records); + $query = <<assertCount(2, $records); $this->assertResults([ - ['field1' => 'foo', 'field2' => 2], - ['field1' => 'bar', 'field2' => 3], + ['field1' => 'foo', 'field2' => 2, 'field3' => 'arg'], + ['field1' => 'bar', 'field2' => 3, 'field3' => 'arg'], ], $records); } @@ -416,36 +444,65 @@ public function testFilterAndSort() $query = <<querySchema($schema, $query); $this->assertSuccess($result); - $this->assertResult('readOneDataObjectFake.title', 'test1', $result); + $this->assertResult('readOneDataObjectFake.myField', 'test1', $result); $query = <<querySchema($schema, $query); $this->assertSuccess($result); - $this->assertResult('readOneDataObjectFake.title', 'test2', $result); + $this->assertResult('readOneDataObjectFake.myField', 'test2', $result); $query = <<querySchema($schema, $query); $this->assertSuccess($result); - $this->assertResult('readOneDataObjectFake.title', 'test1', $result); + $this->assertResult('readOneDataObjectFake.myField', 'test1', $result); + + $query = <<querySchema($schema, $query); + // Nested fields aren't working. Needs refactoring. +// $this->assertSuccess($result); +// $this->assertResult('readOneDataObjectFake.author.firstName', 'tester1', $result); + + $query = <<querySchema($schema, $query); +// $this->assertSuccess($result); +// $this->assertNull($result['data']['readOneDataObjectFake']); } @@ -455,8 +512,11 @@ public function testFieldAliases() $author = Member::create(['FirstName' => 'tester']); $author->write(); - $dataObject = DataObjectFake::create(['MyField' => 'test', 'AuthorID' => $author->ID]); - $dataObject->write(); + $dataObject1 = DataObjectFake::create(['MyField' => 'test1', 'AuthorID' => $author->ID]); + $dataObject1->write(); + + $dataObject2 = DataObjectFake::create(['MyField' => 'test2', 'AuthorID' => $author->ID]); + $dataObject2->write(); $schema = $this->createSchemaFromArray([ 'models' => [ @@ -498,7 +558,7 @@ public function testFieldAliases() $query = <<querySchema($schema, $query); $this->assertSuccess($result); - $this->assertResult('readOneDataObjectFake.myAliasedField', 'test', $result); + $this->assertResult('readOneDataObjectFake.myAliasedField', 'test1', $result); $this->assertResult('readOneDataObjectFake.author.nickname', 'tester', $result); + $query = <<querySchema($schema, $query); + $this->assertSuccess($result); + $this->assertResult('readOneDataObjectFake.myAliasedField', 'test2', $result); + $this->assertResult('readOneDataObjectFake.author', null, $result); + + } + + public function testAggregateProperties() + { + $file1 = File::create(['Title' => '1']); + $file1->write(); + + $file2 = File::create(['Title' => '2']); + $file2->write(); + + $dataObject1 = DataObjectFake::create(['MyField' => 'test1']); + $dataObject1->write(); + + $dataObject2 = DataObjectFake::create(['MyField' => 'test2']); + $dataObject2->write(); + + $dataObject1->Files()->add($file1); + $dataObject1->Files()->add($file2); + + $dataObject1->write(); + + $schema = $this->createSchemaFromArray([ + 'models' => [ + DataObjectFake::class => [ + 'operations' => [ + 'readOne' => [ + 'plugins' => [ + 'filter' => true, + ], + ], + ], + 'fields' => [ + 'myField' => true, + 'fileCount' => [ + 'property' => 'Files.Count()', + 'type' => 'Int', + ], + 'maxFileTitle' => [ + 'property' => 'Files.Max(Title)', + 'type' => 'String', + ], + 'minFileTitle' => [ + 'property' => 'Files.Min(Title)', + 'type' => 'String', + ], + 'fileTitles' => [ + 'property' => 'Files.Title', + 'type' => '[String]', + ], + ] + ] + ] + ]); + + $query = <<ID } }) { + myField + fileCount + maxFileTitle + minFileTitle + fileTitles + } +} +GRAPHQL; + $result = $this->querySchema($schema, $query); + $this->assertSuccess($result); + $this->assertResult('readOneDataObjectFake.myField', 'test1', $result); + $this->assertResult('readOneDataObjectFake.fileCount', 2, $result); + $this->assertResult('readOneDataObjectFake.maxFileTitle', '2', $result); + $this->assertResult('readOneDataObjectFake.minFileTitle', '1', $result); + $arr = $result['data']['readOneDataObjectFake']['fileTitles']; + $this->assertNotNull($arr); + $this->assertCount(2, $arr); + $this->assertTrue(in_array('1', $arr)); + $this->assertTrue(in_array('2', $arr)); + + $query = <<ID } }) { + myField + fileCount + maxFileTitle + minFileTitle + fileTitles + } +} +GRAPHQL; + + $result = $this->querySchema($schema, $query); + $this->assertSuccess($result); + $this->assertResult('readOneDataObjectFake.myField', 'test2', $result); + $this->assertResult('readOneDataObjectFake.fileCount', 0, $result); + $this->assertNull($result['data']['readOneDataObjectFake']['maxFileTitle']); + $this->assertNull($result['data']['readOneDataObjectFake']['minFileTitle']); + $arr = $result['data']['readOneDataObjectFake']['fileTitles']; + $this->assertNotNull($arr); + $this->assertCount(0, $arr); + } + + public function testBasicPaginator() + { + $schema = $this->createSchema(['_' . __FUNCTION__], [IntegrationTestResolver::class]); + $query = <<querySchema($schema, $query); + $this->assertSuccess($result); + $this->assertResult('readMyTypes.pageInfo.totalCount', 100, $result); + $this->assertResult('readMyTypes.pageInfo.hasNextPage', true, $result); + $this->assertResult('readMyTypes.pageInfo.hasPreviousPage', false, $result); + $records = $result['data']['readMyTypes']['nodes'] ?? []; + $this->assertCount(5, $records); + $this->assertResults([ + ['field1' => 'field1-1'], + ['field1' => 'field1-2'], + ['field1' => 'field1-3'], + ['field1' => 'field1-4'], + ['field1' => 'field1-5'], + ], $records); + + $query = <<querySchema($schema, $query); + $this->assertSuccess($result); + $this->assertResult('readMyTypes.pageInfo.totalCount', 100, $result); + $this->assertResult('readMyTypes.pageInfo.hasNextPage', true, $result); + $this->assertResult('readMyTypes.pageInfo.hasPreviousPage', true, $result); + $records = $result['data']['readMyTypes']['nodes'] ?? []; + $this->assertCount(5, $records); + $this->assertResults([ + ['field1' => 'field1-6'], + ['field1' => 'field1-7'], + ['field1' => 'field1-8'], + ['field1' => 'field1-9'], + ['field1' => 'field1-10'], + ], $records); + } private function createSchema(array $configDirs, array $resolvers = [], array $extraConfig = []): GraphQLSchema @@ -583,6 +820,10 @@ private function assertSuccess(array $result) if (!empty($errors)) { $this->fail('Failed to assert successful query. Got errors: ' . json_encode($errors)); } + $error = $result['error'] ?? null; + if ($error) { + $this->fail('Failed to assert successful query. Got error: ' . $error); + } } private function assertFailure(array $result) diff --git a/tests/Schema/_testBasicPaginator/schema.yml b/tests/Schema/_testBasicPaginator/schema.yml new file mode 100644 index 000000000..be354975b --- /dev/null +++ b/tests/Schema/_testBasicPaginator/schema.yml @@ -0,0 +1,12 @@ +types: + MyType: + fields: + field1: String + field2: Int +queries: + readMyTypes: + type: '[MyType]' + resolver: [SilverStripe\GraphQL\Tests\Fake\IntegrationTestResolver, lotsOfMyTypes] + plugins: + paginate: + resolver: [SilverStripe\GraphQL\Tests\Fake\IntegrationTestResolver, testPaginate] diff --git a/tests/Schema/_testNestedFieldDefinitions/models.yml b/tests/Schema/_testNestedFieldDefinitions/models.yml index a9b0a5062..05b568ee5 100644 --- a/tests/Schema/_testNestedFieldDefinitions/models.yml +++ b/tests/Schema/_testNestedFieldDefinitions/models.yml @@ -1,9 +1,9 @@ SilverStripe\GraphQL\Tests\Fake\DataObjectFake: operations: readOne: true - fields: - myField: true - author: - fields: - firstName: true - files: true + fields: + myField: true + author: + fields: + firstName: true + files: true diff --git a/tests/Schema/_testSimpleType/schema.yml b/tests/Schema/_testSimpleType/schema.yml new file mode 100644 index 000000000..3ca06a14f --- /dev/null +++ b/tests/Schema/_testSimpleType/schema.yml @@ -0,0 +1,8 @@ +types: + MyType: + fields: + field1: String + field2: Int + 'field3(MyArg: String)': String +queries: + readMyTypes: '[MyType]'