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

Added ability to control field visibility on schema definition #1434

Merged
merged 13 commits into from
Oct 4, 2023
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` | **function ($context): mixed**<br> You can customize the visibility of fields by defining them as visible or not visible using a `bool` or your own custom logic by sending a `callable` that receives the `$context`. In introspection, the field will not be included in the result and if a query references that field, it will return a validation error. |
carlagouveia marked this conversation as resolved.
Show resolved Hide resolved

### 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;
spawnia marked this conversation as resolved.
Show resolved Hide resolved
}

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
spawnia marked this conversation as resolved.
Show resolved Hide resolved
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