Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,66 @@ func main() {
fmt.Printf("%s \n", rJSON) // {"data":{"hello":"world"}}
}
```

### Federation v2 Support

This library supports GraphQL Federation v2 directives, including `@external` and `@extends`. Here's an example of how to use them:

```go
package main

import (
"github.com/tailor-inc/graphql"
)

func main() {
// Define a type that extends a type from another service
userType := graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
// This field is owned by another service
Directives: []*graphql.Directive{graphql.ExternalDirective},
},
"name": &graphql.Field{
Type: graphql.String,
},
"email": &graphql.Field{
Type: graphql.String,
// This field is also owned by another service
Directives: []*graphql.Directive{graphql.ExternalDirective},
},
},
// This type extends a type from another service
Directives: []*graphql.Directive{graphql.ExtendsDirective},
})

// Use Federation directives in your schema
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: userType,
// Include Federation directives
Directives: append(graphql.SpecifiedDirectives, graphql.FederationDirectives...),
})

if err != nil {
log.Fatalf("failed to create schema: %v", err)
}
}
```

Available Federation v2 directives:
- `@external` - Marks a field as owned by another service
- `@extends` - Allows extending types from other services
- `@requires` - Specifies required fields for resolution
- `@provides` - Specifies fields that will be provided
- `@key` - Defines entity keys
- `@shareable` - Allows fields to be resolved by multiple services
- `@override` - Takes responsibility for a field from another service
- `@inaccessible` - Marks fields as inaccessible to consumers
- `@link` - Links to external schemas
- `@composeDirective` - Preserves custom directives in supergraph

For more complex examples, refer to the [examples/](https://github.com/tailor-inc/graphql/tree/master/examples/) directory and [graphql_test.go](https://github.com/tailor-inc/graphql/blob/master/graphql_test.go).

### Third Party Libraries
Expand Down
35 changes: 32 additions & 3 deletions directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ var SpecifiedDirectives = []*Directive{
DeprecatedDirective,
}

// AllDirectives The full list of specified directives plus Federation directives.
var AllDirectives = append(SpecifiedDirectives, FederationDirectives...)

// FederationDirectives The full list of Federation v2 directives.
var FederationDirectives = []*Directive{
ExternalDirective,
ExtendsDirective,
RequiresDirective,
ProvidesDirective,
KeyDirective,
LinkDirective,
ShareableDirective,
OverrideDirective,
InaccessibleDirective,
ComposeDirective,
}

// Directive structs are used by the GraphQL runtime as a way of modifying execution
// behavior. Type system creators will usually not create these directly.
type Directive struct {
Expand Down Expand Up @@ -147,15 +164,27 @@ var DeprecatedDirective = NewDirective(DirectiveConfig{
},
})

// ExternalDirective
// directive @external on FIELD_DEFINITION
// ExternalDirective The @external directive is used to mark a field as owned by another service.
// This allows you to extend a type from another service without taking ownership of the field.
var ExternalDirective = NewDirective(DirectiveConfig{
Name: "external",
Name: "external",
Description: "The @external directive is used to mark a field as owned by another service. This allows you to extend a type from another service without taking ownership of the field.",
Locations: []string{
DirectiveLocationFieldDefinition,
},
})

// ExtendsDirective The @extends directive is used to represent type extensions in the schema.
// It allows subgraphs to extend types defined in other subgraphs.
var ExtendsDirective = NewDirective(DirectiveConfig{
Name: "extends",
Description: "The @extends directive is used to represent type extensions in the schema. It allows subgraphs to extend types defined in other subgraphs.",
Locations: []string{
DirectiveLocationObject,
DirectiveLocationInterface,
},
})

// RequiresDirective The @requires directive is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services
var RequiresDirective = NewDirective(DirectiveConfig{
Name: "requires",
Expand Down
83 changes: 83 additions & 0 deletions directives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,86 @@ func TestDirectivesWorksWithSkipAndIncludeDirectives_NoIncludeOrSkip(t *testing.
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, result))
}
}

func TestDirectives_ExternalDirective(t *testing.T) {
directive := graphql.ExternalDirective

if directive.Name != "external" {
t.Fatalf("Expected directive name to be 'external', got: %v", directive.Name)
}

expectedDescription := "The @external directive is used to mark a field as owned by another service. This allows you to extend a type from another service without taking ownership of the field."
if directive.Description != expectedDescription {
t.Fatalf("Expected directive description to match, got: %v", directive.Description)
}

expectedLocations := []string{graphql.DirectiveLocationFieldDefinition}
if len(directive.Locations) != len(expectedLocations) {
t.Fatalf("Expected %d locations, got: %d", len(expectedLocations), len(directive.Locations))
}

for i, location := range expectedLocations {
if directive.Locations[i] != location {
t.Fatalf("Expected location %d to be %v, got: %v", i, location, directive.Locations[i])
}
}

if len(directive.Args) != 0 {
t.Fatalf("Expected external directive to have no arguments, got: %d", len(directive.Args))
}
}

func TestDirectives_ExtendsDirective(t *testing.T) {
directive := graphql.ExtendsDirective

if directive.Name != "extends" {
t.Fatalf("Expected directive name to be 'extends', got: %v", directive.Name)
}

expectedDescription := "The @extends directive is used to represent type extensions in the schema. It allows subgraphs to extend types defined in other subgraphs."
if directive.Description != expectedDescription {
t.Fatalf("Expected directive description to match, got: %v", directive.Description)
}

expectedLocations := []string{graphql.DirectiveLocationObject, graphql.DirectiveLocationInterface}
if len(directive.Locations) != len(expectedLocations) {
t.Fatalf("Expected %d locations, got: %d", len(expectedLocations), len(directive.Locations))
}

for i, location := range expectedLocations {
if directive.Locations[i] != location {
t.Fatalf("Expected location %d to be %v, got: %v", i, location, directive.Locations[i])
}
}

if len(directive.Args) != 0 {
t.Fatalf("Expected extends directive to have no arguments, got: %d", len(directive.Args))
}
}

func TestDirectives_FederationDirectives(t *testing.T) {
directives := graphql.FederationDirectives

expectedCount := 10 // external, extends, requires, provides, key, link, shareable, override, inaccessible, composeDirective
if len(directives) != expectedCount {
t.Fatalf("Expected %d federation directives, got: %d", expectedCount, len(directives))
}

// Check that external and extends are included
var foundExternal, foundExtends bool
for _, directive := range directives {
if directive.Name == "external" {
foundExternal = true
}
if directive.Name == "extends" {
foundExtends = true
}
}

if !foundExternal {
t.Fatal("Expected external directive to be in FederationDirectives")
}
if !foundExtends {
t.Fatal("Expected extends directive to be in FederationDirectives")
}
}
76 changes: 76 additions & 0 deletions sdl_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,79 @@ query Example {
t.Log(result)

}

func TestParseSDL_FederationDirectives(t *testing.T) {
sdl := `
directive @external on FIELD_DEFINITION
directive @extends on OBJECT | INTERFACE

type User @extends {
id: ID! @external
name: String
email: String @external
}

type Product {
id: ID!
name: String
user: User
}
`

doc, err := parser.Parse(parser.ParseParams{
Source: sdl,
})
assert.NoError(t, err)

// Check that directives are parsed correctly
var externalDirective, extendsDirective *ast.DirectiveDefinition
var userType *ast.ObjectDefinition

for _, def := range doc.Definitions {
switch node := def.(type) {
case *ast.DirectiveDefinition:
if node.Name.Value == "external" {
externalDirective = node
}
if node.Name.Value == "extends" {
extendsDirective = node
}
case *ast.ObjectDefinition:
if node.Name.Value == "User" {
userType = node
}
}
}

assert.NotNil(t, externalDirective, "external directive should be parsed")
assert.Equal(t, "external", externalDirective.Name.Value)
assert.Equal(t, 1, len(externalDirective.Locations))
assert.Equal(t, "FIELD_DEFINITION", externalDirective.Locations[0].Value)

assert.NotNil(t, extendsDirective, "extends directive should be parsed")
assert.Equal(t, "extends", extendsDirective.Name.Value)
assert.Equal(t, 2, len(extendsDirective.Locations))

assert.NotNil(t, userType, "User type should be parsed")
assert.Equal(t, 1, len(userType.Directives), "User type should have extends directive")
assert.Equal(t, "extends", userType.Directives[0].Name.Value)

// Check fields with external directive
var idField, emailField *ast.FieldDefinition
for _, field := range userType.Fields {
if field.Name.Value == "id" {
idField = field
}
if field.Name.Value == "email" {
emailField = field
}
}

assert.NotNil(t, idField, "id field should exist")
assert.Equal(t, 1, len(idField.Directives), "id field should have external directive")
assert.Equal(t, "external", idField.Directives[0].Name.Value)

assert.NotNil(t, emailField, "email field should exist")
assert.Equal(t, 1, len(emailField.Directives), "email field should have external directive")
assert.Equal(t, "external", emailField.Directives[0].Name.Value)
}