Skip to content

Commit 1bac625

Browse files
committed
Add handling of argument "resolvable" of directive @key
1 parent c0f3941 commit 1bac625

12 files changed

+77
-43
lines changed

README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use GraphQL\Type\Definition\Type;
2222

2323
$userType = new EntityObjectType([
2424
'name' => 'User',
25-
'keyFields' => ['id', 'email'],
25+
'keys' => [['fields' => 'id'], ['fields' => 'email']],
2626
'fields' => [
2727
'id' => ['type' => Type::int()],
2828
'email' => ['type' => Type::string()],
@@ -35,7 +35,7 @@ $userType = new EntityObjectType([
3535
]);
3636
```
3737

38-
* `keyFields` — defines the entity's primary key, which consists of one or more of the fields. An entity's key cannot include fields that return a union or interface.
38+
* `keys` — defines the entity's primary key, which consists of one or more of the fields. An entity's key cannot include fields that return a union or interface.
3939

4040
* `__resolveReference` — resolves the representation of the entity from the provided reference. Subgraphs use representations to reference entities from other subgraphs. A representation requires only an explicit __typename definition and values for the entity's primary key fields.
4141

@@ -50,7 +50,7 @@ use Apollo\Federation\Types\EntityRefObjectType;
5050

5151
$userType = new EntityRefObjectType([
5252
'name' => 'User',
53-
'keyFields' => ['id', 'email'],
53+
'keys' => [['fields' => 'id'], ['fields' => 'email']],
5454
'fields' => [
5555
'id' => ['type' => Type::int()],
5656
'email' => ['type' => Type::string()]
@@ -69,16 +69,12 @@ use Apollo\Federation\Types\EntityRefObjectType;
6969

7070
$userType = new EntityRefObjectType([
7171
'name' => 'User',
72-
'keyFields' => ['id', 'email'],
72+
'keys' => [['fields' => 'id', 'resolvable' => false]],
7373
'fields' => [
7474
'id' => [
7575
'type' => Type::int(),
7676
'isExternal' => true
7777
],
78-
'email' => [
79-
'type' => Type::string(),
80-
'isExternal' => true
81-
]
8278
]
8379
]);
8480
```

src/Directives/KeyDirective.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,28 @@
1111
/**
1212
* The `@key` directive is used to indicate a combination of fields that can be used to uniquely
1313
* identify and fetch an object or interface.
14+
*
15+
* @see https://www.apollographql.com/docs/federation/federated-types/federated-directives/#key
1416
*/
1517
class KeyDirective extends Directive
1618
{
19+
public const ARGUMENT_FIELDS = 'fields';
20+
public const ARGUMENT_RESOLVABLE = 'resolvable';
21+
1722
public function __construct()
1823
{
1924
parent::__construct([
2025
'name' => DirectiveEnum::KEY,
2126
'locations' => [DirectiveLocation::OBJECT, DirectiveLocation::IFACE],
2227
'args' => [
2328
new FieldArgument([
24-
'name' => 'fields',
29+
'name' => self::ARGUMENT_FIELDS,
2530
'type' => Type::nonNull(Type::string()),
2631
]),
32+
new FieldArgument([
33+
'name' => self::ARGUMENT_RESOLVABLE,
34+
'type' => Type::boolean(),
35+
]),
2736
],
2837
]);
2938
}

src/FederatedSchema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* 'firstName' => [...],
3737
* 'lastName' => [...],
3838
* ],
39-
* 'keyFields' => ['id', 'email']
39+
* 'keys' => [['fields' => 'id'], ['fields' => 'email']]
4040
* ]);
4141
*
4242
* $queryType = new GraphQL\Type\Definition\ObjectType([

src/Types/EntityObjectType.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* <code>
2323
* $userType = new Apollo\Federation\Types\EntityObjectType([
2424
* 'name' => 'User',
25-
* 'keyFields' => ['id', 'email'],
25+
* 'keys' => [['fields' => 'id'], ['fields' => 'email']],
2626
* 'fields' => [...]
2727
* ]);
2828
* </code>
@@ -31,7 +31,7 @@
3131
* <code>
3232
* $userType = new Apollo\Federation\Types\EntityObjectType([
3333
* 'name' => 'User',
34-
* 'keyFields' => ['id', 'email'],
34+
* 'keys' => [['fields' => 'id', 'resolvable': false ]],
3535
* 'fields' => [
3636
* 'id' => [
3737
* 'type' => Types::int(),
@@ -43,7 +43,7 @@
4343
*/
4444
class EntityObjectType extends ObjectType
4545
{
46-
public const FIELD_KEY_FIELDS = 'keyFields';
46+
public const FIELD_KEYS = 'keys';
4747
public const FIELD_REFERENCE_RESOLVER = '__resolveReference';
4848

4949
public const FIELD_DIRECTIVE_IS_EXTERNAL = 'isExternal';
@@ -54,16 +54,22 @@ class EntityObjectType extends ObjectType
5454
public $referenceResolver = null;
5555

5656
/**
57-
* @var array<int,string>|array<int|string,string|array<int|string,mixed>>
57+
* @var array<int,array{fields: array<int,string>|array<int|string,string|array<int|string,mixed>>, resolvable: bool }>
5858
*/
59-
private array $keyFields;
59+
private array $keys;
6060

6161
/**
6262
* @param array<string,mixed> $config
6363
*/
6464
public function __construct(array $config)
6565
{
66-
$this->keyFields = $config[self::FIELD_KEY_FIELDS];
66+
Utils::invariant(
67+
!(\array_key_exists(self::FIELD_KEYS, $config) && \array_key_exists('keyFields', $config)),
68+
'Use only one way to define directives @key.'
69+
);
70+
71+
$this->keys = $config[self::FIELD_KEYS]
72+
?? array_map(static fn ($x): array => ['fields' => $x], $config['keyFields']);
6773

6874
if (isset($config[self::FIELD_REFERENCE_RESOLVER])) {
6975
self::validateResolveReference($config);
@@ -76,11 +82,30 @@ public function __construct(array $config)
7682
/**
7783
* Gets the fields that serve as the unique key or identifier of the entity.
7884
*
85+
* @deprecated Use {@see getKeys()}
86+
*
7987
* @return array<int,string>|array<int|string,string|array<int|string,mixed>>
8088
*/
8189
public function getKeyFields(): array
8290
{
83-
return $this->keyFields;
91+
@trigger_error(
92+
'Since skillshare/apollo-federation-php 2.0.0: '
93+
. 'Method \Apollo\Federation\Types\EntityObjectType::getKeyFields() is deprecated. '
94+
. 'Use \Apollo\Federation\Types\EntityObjectType::getKeys() instead of it.',
95+
\E_USER_DEPRECATED
96+
);
97+
98+
return array_map(static fn(array $x) => $x['fields'], $this->keys);
99+
}
100+
101+
/**
102+
* Gets the fields that serve as the unique key or identifier of the entity.
103+
*
104+
* @return array<int,array{fields: array<int,string>|array<int|string,string|array<int|string,mixed>>, resolvable: bool }>
105+
*/
106+
public function getKeys(): array
107+
{
108+
return $this->keys;
84109
}
85110

86111
/**

src/Utils/FederatedSchemaPrinter.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
namespace Apollo\Federation\Utils;
3333

34+
use Apollo\Federation\Directives\KeyDirective;
3435
use Apollo\Federation\Enum\DirectiveEnum;
3536
use Apollo\Federation\FederatedSchema;
3637
use Apollo\Federation\Types\EntityObjectType;
@@ -192,8 +193,12 @@ protected static function printKeyDirective(EntityObjectType $type): string
192193
{
193194
$keyDirective = '';
194195

195-
foreach ($type->getKeyFields() as $keyField) {
196-
$keyDirective .= sprintf(' @key(fields: "%s")', static::printKeyFields($keyField));
196+
foreach ($type->getKeys() as $keyField) {
197+
$arguments = [sprintf('%s: "%s"', KeyDirective::ARGUMENT_FIELDS, static::printKeyFields($keyField['fields']))];
198+
if (\array_key_exists('resolvable', $keyField)) {
199+
$arguments[] = sprintf('%s: %s', KeyDirective::ARGUMENT_RESOLVABLE, $keyField['resolvable'] ? 'true' : 'false');
200+
}
201+
$keyDirective .= sprintf(' @key(%s)', implode(', ', $arguments));
197202
}
198203

199204
return $keyDirective;

test/EntitiesTest.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ class EntitiesTest extends TestCase
1616

1717
public function testCreatingEntityType(): void
1818
{
19-
$expectedKeyFields = ['id', 'email'];
19+
$expectedKeys = [['fields' => 'id'], ['fields' => 'email']];
2020

2121
$userType = new EntityObjectType([
2222
'name' => 'User',
23-
'keyFields' => $expectedKeyFields,
23+
'keys' => $expectedKeys,
2424
'fields' => [
2525
'id' => ['type' => Type::int()],
2626
'email' => ['type' => Type::string()],
@@ -29,17 +29,17 @@ public function testCreatingEntityType(): void
2929
],
3030
]);
3131

32-
$this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields());
32+
$this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys());
3333
$this->assertMatchesSnapshot($userType->config);
3434
}
3535

3636
public function testCreatingEntityTypeWithCallable(): void
3737
{
38-
$expectedKeyFields = ['id', 'email'];
38+
$expectedKeys = [['fields' => 'id'], ['fields' => 'email']];
3939

4040
$userType = new EntityObjectType([
4141
'name' => 'User',
42-
'keyFields' => $expectedKeyFields,
42+
'keys' => $expectedKeys,
4343
'fields' => function () {
4444
return [
4545
'id' => ['type' => Type::int()],
@@ -50,7 +50,7 @@ public function testCreatingEntityTypeWithCallable(): void
5050
},
5151
]);
5252

53-
$this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields());
53+
$this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys());
5454
$this->assertMatchesSnapshot($userType->config);
5555
}
5656

@@ -66,7 +66,7 @@ public function testResolvingEntityReference(): void
6666

6767
$userType = new EntityObjectType([
6868
'name' => 'User',
69-
'keyFields' => ['id', 'email'],
69+
'keys' => [['fields' => 'id'], ['fields' => 'email']],
7070
'fields' => [
7171
'id' => ['type' => Type::int()],
7272
'email' => ['type' => Type::string()],
@@ -85,18 +85,18 @@ public function testResolvingEntityReference(): void
8585

8686
public function testCreatingEntityRefType(): void
8787
{
88-
$expectedKeyFields = ['id', 'email'];
88+
$expectedKeys = [['fields' => 'id', 'resolvable' => false]];
8989

9090
$userType = new EntityRefObjectType([
9191
'name' => 'User',
92-
'keyFields' => $expectedKeyFields,
92+
'keys' => $expectedKeys,
9393
'fields' => [
9494
'id' => ['type' => Type::int()],
9595
'email' => ['type' => Type::string()],
9696
],
9797
]);
9898

99-
$this->assertEqualsCanonicalizing($expectedKeyFields, $userType->getKeyFields());
99+
$this->assertEqualsCanonicalizing($expectedKeys, $userType->getKeys());
100100
$this->assertMatchesSnapshot($userType->config);
101101
}
102102
}

test/StarWarsSchema.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private static function getEpisodeType(): EntityObjectType
8383
'provides' => 'name',
8484
],
8585
],
86-
EntityObjectType::FIELD_KEY_FIELDS => ['id'],
86+
EntityObjectType::FIELD_KEYS => ['fields' => 'id'],
8787
EntityObjectType::FIELD_REFERENCE_RESOLVER => static function (array $ref): array {
8888
$entity = StarWarsData::getEpisodeById($ref['id']);
8989
$entity['__typename'] = 'Episode';
@@ -113,7 +113,7 @@ private static function getCharacterType(): EntityRefObjectType
113113
'requires' => 'name',
114114
],
115115
],
116-
EntityObjectType::FIELD_KEY_FIELDS => ['id'],
116+
EntityObjectType::FIELD_KEYS => ['fields' => 'id'],
117117
]);
118118
}
119119

@@ -132,7 +132,7 @@ private static function getLocationType(): EntityRefObjectType
132132
'isExternal' => true,
133133
],
134134
],
135-
EntityObjectType::FIELD_KEY_FIELDS => ['id'],
135+
EntityObjectType::FIELD_KEYS => ['fields' => 'id'],
136136
]);
137137
}
138138
}

test/__snapshots__/DirectivesTest__testItAddsDirectivesToSchema__1.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ directive @external on FIELD_DEFINITION
22

33
directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION
44

5-
directive @key(fields: String!) on OBJECT | INTERFACE
5+
directive @key(fields: String!, resolvable: Boolean) on OBJECT | INTERFACE
66

77
directive @link(url: String!, as: String, for: String, import: [link_Import!]) repeatable on SCHEMA
88

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
name: User
2-
keyFields:
3-
- id
4-
- email
2+
keys:
3+
- { fields: id, resolvable: false }
54
fields:
65
id: { type: { name: Int, description: "The `Int` scalar type represents non-fractional signed whole numeric\nvalues. Int can represent values between -(2^31) and 2^31 - 1. ", astNode: null, extensionASTNodes: null, config: { } } }
76
email: { type: { name: String, description: "The `String` scalar type represents textual data, represented as UTF-8\ncharacter sequences. The String type is most often used by GraphQL to\nrepresent free-form human-readable text.", astNode: null, extensionASTNodes: null, config: { } } }
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: User
2-
keyFields:
3-
- id
4-
- email
2+
keys:
3+
- { fields: id }
4+
- { fields: email }
55
fields: { }

0 commit comments

Comments
 (0)