Skip to content

Commit c0f3941

Browse files
committed
Implement directive @link
1 parent 17bf125 commit c0f3941

11 files changed

+264
-29
lines changed

src/Directives.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Apollo\Federation\Directives\ExternalDirective;
88
use Apollo\Federation\Directives\InaccessibleDirective;
99
use Apollo\Federation\Directives\KeyDirective;
10+
use Apollo\Federation\Directives\LinkDirective;
1011
use Apollo\Federation\Directives\OverrideDirective;
1112
use Apollo\Federation\Directives\ProvidesDirective;
1213
use Apollo\Federation\Directives\RequiresDirective;
@@ -23,6 +24,7 @@ class Directives
2324
* external: ExternalDirective,
2425
* inaccessible: InaccessibleDirective,
2526
* key: KeyDirective,
27+
* link: LinkDirective,
2628
* override: OverrideDirective,
2729
* requires: RequiresDirective,
2830
* provides: ProvidesDirective,
@@ -55,6 +57,14 @@ public static function inaccessible(): InaccessibleDirective
5557
return self::getDirectives()[DirectiveEnum::INACCESSIBLE];
5658
}
5759

60+
/**
61+
* Gets the `link` directive.
62+
*/
63+
public static function link(): LinkDirective
64+
{
65+
return self::getDirectives()[DirectiveEnum::LINK];
66+
}
67+
5868
/**
5969
* Gets the @override directive.
6070
*/
@@ -94,6 +104,7 @@ public static function shareable(): ShareableDirective
94104
* external: ExternalDirective,
95105
* inaccessible: InaccessibleDirective,
96106
* key: KeyDirective,
107+
* link: LinkDirective,
97108
* override: OverrideDirective,
98109
* requires: RequiresDirective,
99110
* provides: ProvidesDirective,
@@ -107,6 +118,7 @@ public static function getDirectives(): array
107118
DirectiveEnum::EXTERNAL => new ExternalDirective(),
108119
DirectiveEnum::INACCESSIBLE => new InaccessibleDirective(),
109120
DirectiveEnum::KEY => new KeyDirective(),
121+
DirectiveEnum::LINK => new LinkDirective(),
110122
DirectiveEnum::OVERRIDE => new OverrideDirective(),
111123
DirectiveEnum::REQUIRES => new RequiresDirective(),
112124
DirectiveEnum::PROVIDES => new ProvidesDirective(),

src/Directives/LinkDirective.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Apollo\Federation\Directives;
4+
5+
use Apollo\Federation\Enum\DirectiveEnum;
6+
use GraphQL\Error\InvariantViolation;
7+
use GraphQL\Language\DirectiveLocation;
8+
use GraphQL\Type\Definition\CustomScalarType;
9+
use GraphQL\Type\Definition\Directive;
10+
use GraphQL\Type\Definition\FieldArgument;
11+
use GraphQL\Type\Definition\Type;
12+
13+
/**
14+
* The `@link` directive is used to ...
15+
*
16+
* @see https://specs.apollo.dev/link/v1.0/
17+
*/
18+
class LinkDirective extends Directive
19+
{
20+
public function __construct()
21+
{
22+
$linkImport = new CustomScalarType([
23+
'name' => 'link_Import',
24+
'serialize' => static fn ($value) => json_encode($value, \JSON_THROW_ON_ERROR),
25+
'parseValue' => static function ($value) {
26+
if (\is_string($value)) {
27+
return $value;
28+
}
29+
30+
if (!\is_array($value)) {
31+
throw new InvariantViolation('"link_Import" must be a string or an array');
32+
}
33+
$permittedKeys = ['name', 'as'];
34+
$keys = array_keys($value);
35+
if ($permittedKeys !== array_intersect($permittedKeys, $keys) || array_diff($keys, $permittedKeys)) {
36+
throw new InvariantViolation('"link_Import" must contain only keys "name" and "as" and they are required');
37+
}
38+
39+
if (2 !== \count(array_filter($value))) {
40+
throw new InvariantViolation('The "name" and "as" part of "link_Import" be not empty');
41+
}
42+
43+
$prefixes = array_unique([$value['name'][0], $value['as'][0]]);
44+
if (\in_array('@', $prefixes, true) && 2 !== \count($prefixes)) {
45+
// https://specs.apollo.dev/link/v1.0/#Import
46+
throw new InvariantViolation('The "name" and "as" part of "link_Import" be of the same type');
47+
}
48+
49+
return $value;
50+
},
51+
]);
52+
53+
parent::__construct([
54+
'name' => DirectiveEnum::LINK,
55+
'locations' => [DirectiveLocation::SCHEMA],
56+
'args' => [
57+
new FieldArgument([
58+
'name' => 'url',
59+
'type' => Type::nonNull(Type::string()),
60+
]),
61+
new FieldArgument([
62+
'name' => 'as',
63+
'type' => Type::string(),
64+
]),
65+
new FieldArgument([
66+
'name' => 'for',
67+
// TODO use union type (enum & string) and declare required enum
68+
'type' => Type::string(),
69+
]),
70+
new FieldArgument([
71+
'name' => 'import',
72+
'type' => Type::listOf(Type::nonNull($linkImport)),
73+
]),
74+
],
75+
'isRepeatable' => true,
76+
]);
77+
}
78+
}

src/Enum/DirectiveEnum.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class DirectiveEnum
99
public const EXTERNAL = 'external';
1010
public const INACCESSIBLE = 'inaccessible';
1111
public const KEY = 'key';
12+
public const LINK = 'link';
1213
public const OVERRIDE = 'override';
1314
public const PROVIDES = 'provides';
1415
public const REQUIRES = 'requires';

src/FederatedSchema.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public function __construct(array $config)
8282
{
8383
$this->entityTypes = $this->extractEntityTypes($config);
8484
$this->entityDirectives = Directives::getDirectives();
85+
$this->schemaExtensionTypes = $this->extractSchemaExtensionTypes($config);
8586

8687
$config = array_merge($config, $this->getEntityDirectivesConfig($config), $this->getQueryTypeConfig($config));
8788

src/FederatedSchemaTrait.php

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Apollo\Federation;
66

77
use Apollo\Federation\Types\EntityObjectType;
8+
use Apollo\Federation\Types\SchemaExtensionType;
89
use Apollo\Federation\Utils\FederatedSchemaPrinter;
910
use GraphQL\Type\Definition\CustomScalarType;
1011
use GraphQL\Type\Definition\Directive;
@@ -22,6 +23,11 @@ trait FederatedSchemaTrait
2223
*/
2324
protected array $entityTypes = [];
2425

26+
/**
27+
* @var SchemaExtensionType[]
28+
*/
29+
protected array $schemaExtensionTypes = [];
30+
2531
/**
2632
* @var Directive[]
2733
*/
@@ -42,7 +48,15 @@ public function getEntityTypes(): array
4248
*/
4349
public function hasEntityTypes(): bool
4450
{
45-
return !empty($this->getEntityTypes());
51+
return !empty($this->entityTypes);
52+
}
53+
54+
/**
55+
* @return SchemaExtensionType[]
56+
*/
57+
public function getSchemaExtensionTypes(): array
58+
{
59+
return $this->schemaExtensionTypes;
4660
}
4761

4862
/**
@@ -59,22 +73,25 @@ protected function getEntityDirectivesConfig(array $config): array
5973
}
6074

6175
/**
62-
* @param array<string,mixed> $config
76+
* @param array{ query: ObjectType } $config
6377
*
6478
* @return array{ query: ObjectType }
6579
*/
6680
protected function getQueryTypeConfig(array $config): array
6781
{
6882
$queryTypeConfig = $config['query']->config;
69-
if (\is_callable($queryTypeConfig['fields'])) {
70-
$queryTypeConfig['fields'] = $queryTypeConfig['fields']();
71-
}
83+
$fields = $queryTypeConfig['fields'];
84+
$queryTypeConfig['fields'] = function () use ($config, $fields) {
85+
if (\is_callable($fields)) {
86+
$fields = $fields();
87+
}
7288

73-
$queryTypeConfig['fields'] = array_merge(
74-
$queryTypeConfig['fields'],
75-
$this->getQueryTypeServiceFieldConfig(),
76-
$this->getQueryTypeEntitiesFieldConfig($config)
77-
);
89+
return array_merge(
90+
$fields,
91+
$this->getQueryTypeServiceFieldConfig(),
92+
$this->getQueryTypeEntitiesFieldConfig($config)
93+
);
94+
};
7895

7996
return [
8097
'query' => new ObjectType($queryTypeConfig),
@@ -87,17 +104,17 @@ protected function getQueryTypeConfig(array $config): array
87104
protected function getQueryTypeServiceFieldConfig(): array
88105
{
89106
$serviceType = new ObjectType([
90-
'name' => self::RESERVED_TYPE_SERVICE,
107+
'name' => FederatedSchema::RESERVED_TYPE_SERVICE,
91108
'fields' => [
92-
self::RESERVED_FIELD_SDL => [
109+
FederatedSchema::RESERVED_FIELD_SDL => [
93110
'type' => Type::string(),
94111
'resolve' => fn (): string => FederatedSchemaPrinter::doPrint($this),
95112
],
96113
],
97114
]);
98115

99116
return [
100-
self::RESERVED_FIELD_SERVICE => [
117+
FederatedSchema::RESERVED_FIELD_SERVICE => [
101118
'type' => Type::nonNull($serviceType),
102119
'resolve' => static fn (): array => [],
103120
],
@@ -111,25 +128,25 @@ protected function getQueryTypeServiceFieldConfig(): array
111128
*/
112129
protected function getQueryTypeEntitiesFieldConfig(?array $config): array
113130
{
114-
if (!$this->hasEntityTypes()) {
131+
if (!$this->entityTypes) {
115132
return [];
116133
}
117134

118135
$entityType = new UnionType([
119-
'name' => self::RESERVED_TYPE_ENTITY,
136+
'name' => FederatedSchema::RESERVED_TYPE_ENTITY,
120137
'types' => array_values($this->getEntityTypes()),
121138
]);
122139

123140
$anyType = new CustomScalarType([
124-
'name' => self::RESERVED_TYPE_ANY,
141+
'name' => FederatedSchema::RESERVED_TYPE_ANY,
125142
'serialize' => static fn ($value) => $value,
126143
]);
127144

128145
return [
129-
self::RESERVED_FIELD_ENTITIES => [
146+
FederatedSchema::RESERVED_FIELD_ENTITIES => [
130147
'type' => Type::listOf($entityType),
131148
'args' => [
132-
self::RESERVED_FIELD_REPRESENTATIONS => [
149+
FederatedSchema::RESERVED_FIELD_REPRESENTATIONS => [
133150
'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType))),
134151
],
135152
],
@@ -147,25 +164,24 @@ protected function getQueryTypeEntitiesFieldConfig(?array $config): array
147164
protected function resolve($root, $args, $context, $info): array
148165
{
149166
return array_map(static function ($ref) use ($context, $info) {
150-
Utils::invariant(isset($ref[self::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.');
167+
Utils::invariant(isset($ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME]), 'Type name must be provided in the reference.');
151168

152-
$typeName = $ref[self::RESERVED_FIELD_TYPE_NAME];
169+
$typeName = $ref[FederatedSchema::RESERVED_FIELD_TYPE_NAME];
153170
$type = $info->schema->getType($typeName);
154171

155172
Utils::invariant(
156-
$type && $type instanceof EntityObjectType,
157-
sprintf(
158-
'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema',
159-
$type->name
160-
)
173+
$type instanceof EntityObjectType,
174+
'The _entities resolver tried to load an entity for type "%s", but no object type of that name was found in the schema',
175+
$type->name
161176
);
162177

178+
/** @var EntityObjectType $type */
163179
if (!$type->hasReferenceResolver()) {
164180
return $ref;
165181
}
166182

167183
return $type->resolveReference($ref, $context, $info);
168-
}, $args[self::RESERVED_FIELD_REPRESENTATIONS]);
184+
}, $args[FederatedSchema::RESERVED_FIELD_REPRESENTATIONS]);
169185
}
170186

171187
/**
@@ -186,4 +202,30 @@ protected function extractEntityTypes(array $config): array
186202

187203
return $entityTypes;
188204
}
205+
206+
/**
207+
* @param array<string,mixed> $config
208+
*
209+
* @return SchemaExtensionType[]
210+
*/
211+
protected function extractSchemaExtensionTypes(array $config): array
212+
{
213+
$typeMap = [];
214+
$configTypes = $config['types'] ?? [];
215+
if (\is_array($configTypes)) {
216+
$typeMap = $configTypes;
217+
} elseif (\is_callable($configTypes)) {
218+
$typeMap = $configTypes();
219+
}
220+
221+
$types = [];
222+
223+
foreach ($typeMap as $type) {
224+
if ($type instanceof SchemaExtensionType) {
225+
$types[$type->name] = $type;
226+
}
227+
}
228+
229+
return $types;
230+
}
189231
}

src/Types/SchemaExtensionType.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Apollo\Federation\Types;
6+
7+
use GraphQL\Type\Definition\CompositeType;
8+
use GraphQL\Type\Definition\NamedType;
9+
use GraphQL\Type\Definition\OutputType;
10+
use GraphQL\Type\Definition\Type;
11+
12+
class SchemaExtensionType extends Type implements OutputType, CompositeType, NamedType
13+
{
14+
public const FIELD_KEY_LINKS = 'links';
15+
16+
/**
17+
* @param array<string,mixed> $config
18+
*/
19+
public function __construct(array $config)
20+
{
21+
$this->config = $config;
22+
}
23+
}

0 commit comments

Comments
 (0)