Skip to content

Commit

Permalink
Added ability to control field visibility on schema definition (#1434)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlagouveia authored Oct 4, 2023
1 parent d86cefa commit 2af585f
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 32 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bench: ## Runs benchmarks with phpbench
.PHONY: docs
docs: ## Generate the class-reference docs
php generate-class-reference.php
prettier --write docs/class-reference.md

vendor: composer.json composer.lock
composer install
Expand Down
17 changes: 9 additions & 8 deletions docs/type-definitions/object-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ This example uses **inline** style for Object Type definitions, but you can also

## Configuration options

| Option | Type | Notes |
| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| name | `string` | **Required.** Unique name of this object type within Schema |
| fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array. See [field configuration options](#field-configuration-options) section below for expected structure of each array entry. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
| description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) |
| interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
| isTypeOf | `callable` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): bool**<br> Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation). |
| resolveField | `callable` | **function ($value, array $args, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): mixed**<br> Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details. |
| Option | Type | Notes |
|--------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | `string` | **Required.** Unique name of this object type within Schema |
| fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array. See [field configuration options](#field-configuration-options) section below for expected structure of each array entry. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
| description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) |
| interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
| isTypeOf | `callable` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): bool**<br> Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation). |
| resolveField | `callable` | **function ($value, array $args, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): mixed**<br> Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details. |
| visible | `bool` or `callable` | Defaults to `true`. The given callable receives no arguments and is expected to return a `bool`, it is called once when the field may be accessed. The field is treated as if it were not defined at all when this is `false`. |

### Field configuration options

Expand Down
2 changes: 1 addition & 1 deletion src/Executor/ReferenceExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ protected function resolveField(ObjectType $parentType, $rootValue, \ArrayObject

$fieldName = $fieldNode->name->value;
$fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
if ($fieldDef === null) {
if ($fieldDef === null || ! $fieldDef->isVisible()) {
return static::$UNDEFINED;
}

Expand Down
20 changes: 20 additions & 0 deletions src/Type/Definition/FieldDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
*
* @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType)
* @phpstan-type ComplexityFn callable(int, array<string, mixed>): int
* @phpstan-type VisibilityFn callable(): bool
* @phpstan-type FieldDefinitionConfig array{
* name: string,
* type: FieldType,
* resolve?: FieldResolver|null,
* args?: ArgumentListConfig|null,
* description?: string|null,
* visible?: VisibilityFn|bool,
* deprecationReason?: string|null,
* astNode?: FieldDefinitionNode|null,
* complexity?: ComplexityFn|null
Expand All @@ -31,6 +33,7 @@
* resolve?: FieldResolver|null,
* args?: ArgumentListConfig|null,
* description?: string|null,
* visible?: VisibilityFn|bool,
* deprecationReason?: string|null,
* astNode?: FieldDefinitionNode|null,
* complexity?: ComplexityFn|null
Expand Down Expand Up @@ -64,6 +67,13 @@ class FieldDefinition

public ?string $description;

/**
* @var callable|bool
*
* @phpstan-var VisibilityFn|bool
*/
public $visible;

public ?string $deprecationReason;

public ?FieldDefinitionNode $astNode;
Expand Down Expand Up @@ -94,6 +104,7 @@ public function __construct(array $config)
? Argument::listFromConfig($config['args'])
: [];
$this->description = $config['description'] ?? null;
$this->visible = $config['visible'] ?? true;
$this->deprecationReason = $config['deprecationReason'] ?? null;
$this->astNode = $config['astNode'] ?? null;
$this->complexityFn = $config['complexity'] ?? null;
Expand Down Expand Up @@ -181,6 +192,15 @@ public function getType(): Type
return $this->type ??= Schema::resolveType($this->config['type']);
}

public function isVisible(): bool
{
if (is_bool($this->visible)) {
return $this->visible;
}

return $this->visible = ($this->visible)();
}

public function isDeprecated(): bool
{
return (bool) $this->deprecationReason;
Expand Down
9 changes: 9 additions & 0 deletions src/Type/Definition/HasFieldsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public function findField(string $name): ?FieldDefinition;
public function getFields(): array;

/**
* @throws InvariantViolation
*
* @return array<string, FieldDefinition>
*/
public function getVisibleFields(): array;

/**
* Get all field names, including only visible fields.
*
* @throws InvariantViolation
*
* @return array<int, string>
Expand Down
15 changes: 14 additions & 1 deletion src/Type/Definition/HasFieldsTypeImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,24 @@ public function getFields(): array
return $this->fields;
}

public function getVisibleFields(): array
{
return array_filter(
$this->getFields(),
fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible()
);
}

/** @throws InvariantViolation */
public function getFieldNames(): array
{
$this->initializeFields();

return \array_keys($this->fields);
$visibleFieldNames = array_map(
fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(),
$this->getVisibleFields()
);

return array_values($visibleFieldNames);
}
}
27 changes: 9 additions & 18 deletions src/Type/Introspection.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,12 @@ public static function _type(): ObjectType
],
'resolve' => static function (Type $type, $args): ?array {
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$fields = $type->getFields();
$fields = $type->getVisibleFields();

if (! ($args['includeDeprecated'] ?? false)) {
$fields = \array_filter(
return \array_filter(
$fields,
static fn (FieldDefinition $field): bool => $field->deprecationReason === null
|| $field->deprecationReason === ''
static fn (FieldDefinition $field): bool => ! $field->isDeprecated()
);
}

Expand Down Expand Up @@ -397,10 +396,7 @@ public static function _type(): ObjectType
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$values,
static function (EnumValueDefinition $value): bool {
return $value->deprecationReason === null
|| $value->deprecationReason === '';
}
static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated()
);
}

Expand All @@ -425,8 +421,7 @@ static function (EnumValueDefinition $value): bool {
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$fields,
static fn (InputObjectField $field): bool => $field->deprecationReason === null
|| $field->deprecationReason === '',
static fn (InputObjectField $field): bool => ! $field->isDeprecated(),
);
}

Expand Down Expand Up @@ -521,8 +516,7 @@ public static function _field(): ObjectType
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$values,
static fn (Argument $value): bool => $value->deprecationReason === null
|| $value->deprecationReason === '',
static fn (Argument $value): bool => ! $value->isDeprecated(),
);
}

Expand All @@ -535,8 +529,7 @@ public static function _field(): ObjectType
],
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
'resolve' => static fn (FieldDefinition $field): bool => $field->deprecationReason !== null
&& $field->deprecationReason !== '',
'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
Expand Down Expand Up @@ -593,8 +586,7 @@ public static function _inputValue(): ObjectType
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
/** @param Argument|InputObjectField $inputValue */
'resolve' => static fn ($inputValue): bool => $inputValue->deprecationReason !== null
&& $inputValue->deprecationReason !== '',
'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
Expand Down Expand Up @@ -625,8 +617,7 @@ public static function _enumValue(): ObjectType
],
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->deprecationReason !== null
&& $enumValue->deprecationReason !== '',
'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Rules/FieldsOnCorrectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function getVisitor(QueryValidationContext $context): array
return [
NodeKind::FIELD => function (FieldNode $node) use ($context): void {
$fieldDef = $context->getFieldDef();
if ($fieldDef !== null) {
if ($fieldDef !== null && $fieldDef->isVisible()) {
return;
}

Expand Down
22 changes: 19 additions & 3 deletions tests/Executor/ExecutorSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,22 @@ public function testExecutesUsingASchema(): void
'name' => 'Image',
'fields' => [
'url' => ['type' => Type::string()],
'width' => ['type' => Type::int()],
'height' => ['type' => Type::int()],
'width' => [
'type' => Type::int(),
'visible' => fn (): bool => true,
],
'height' => [
'type' => Type::int(),
'visible' => true,
],
'mimetype' => [
'type' => Type::string(),
'visible' => fn (): bool => false,
],
'size' => [
'type' => Type::string(),
'visible' => false,
],
],
]);

Expand Down Expand Up @@ -107,7 +121,8 @@ public function testExecutesUsingASchema(): void
pic(width: 640, height: 480) {
url,
width,
height
height,
mimetype
},
recentArticle {
...articleFields,
Expand Down Expand Up @@ -222,6 +237,7 @@ private function article(string $id): array
'url' => "cdn://{$uid}",
'width' => $width,
'height' => $height,
'mimetype' => 'image/gif',
];

$johnSmith = [
Expand Down
5 changes: 5 additions & 0 deletions tests/StarWarsSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public static function build(): Schema
'type' => Type::string(),
'description' => 'All secrets about their past.',
],
'secretName' => [
'type' => Type::string(),
'description' => 'The secret name of the character.',
'visible' => false,
],
];
},
'resolveType' => static function (array $obj) use (&$humanType, &$droidType): ObjectType {
Expand Down
13 changes: 13 additions & 0 deletions tests/StarWarsValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ public function testThatNonExistentFieldsAreInvalid(): void
self::assertCount(1, $errors);
}

public function testThatInvisibleFieldsAreInvalid(): void
{
$query = '
query HeroSpaceshipQuery {
hero {
secretName
}
}
';
$errors = $this->validationErrors($query);
self::assertCount(1, $errors);
}

/** @see it('Requires fields on objects') */
public function testRequiresFieldsOnObjects(): void
{
Expand Down
Loading

0 comments on commit 2af585f

Please sign in to comment.