Skip to content

Commit

Permalink
Add support for custom schema prototype (#90)
Browse files Browse the repository at this point in the history
* Support using a custom schema prototype
  • Loading branch information
cschiewek authored Feb 7, 2024
1 parent e3fabcd commit ef3ca32
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 139 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ defmodule MyApp.MySchema do
end
```

### Macro based schema with existing prototype

If you are already using a schema prototype

```elixir
defmodule MyApp.MySchema do
use Absinthe.Schema
+ use Absinthe.Federation.Schema, skip_prototype: true

@prototype_schema MyApp.MySchemaPrototype

query do
...
end
end
```

```elixir
defmodule MyApp.MySchemaPrototype do
use Absinthe.Schema.Prototype
+ use Absinthe.Federation.Schema.Prototype.FederatedDirectives

directive :my_directive do
on [:schema]
end
end
```

### SDL based schemas (experimental)

```elixir
Expand Down
7 changes: 5 additions & 2 deletions lib/absinthe/federation/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ defmodule Absinthe.Federation.Schema do
do_using(opts)
end

defp do_using(_opts) do
defp do_using(opts) do
quote do
@pipeline_modifier unquote(__MODULE__)
@prototype_schema Absinthe.Federation.Schema.Prototype

unless unquote(opts[:skip_prototype]) do
@prototype_schema Absinthe.Federation.Schema.Prototype
end

use Absinthe.Federation.Notation
import_types Absinthe.Federation.Types
Expand Down
138 changes: 1 addition & 137 deletions lib/absinthe/federation/schema/prototype.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,141 +2,5 @@ defmodule Absinthe.Federation.Schema.Prototype do
@moduledoc false

use Absinthe.Schema.Prototype

@desc """
_FieldSet is a custom scalar type that is used to represent a set of fields.
Grammatically, a field set is a selection set minus the braces.
This means it can represent a single field "upc", multiple fields "id countryCode",
and even nested selection sets "id organization { id }"
"""
scalar :_field_set, name: "_FieldSet" do
serialize & &1
parse &{:ok, &1}
end

enum :link_purpose, name: "link__Purpose" do
value :security
value :execution
end

scalar :link_import, name: "link__Import" do
serialize & &1
parse &{:ok, &1}
end

@desc """
The `@key` directive is used to indicate a combination of fields that can be used
to uniquely identify and fetch an object or interface.
"""
directive :key do
arg :fields, non_null(:_field_set)
arg :resolvable, :boolean, default_value: true
repeatable true
on [:object, :interface]
end

@desc """
The @external directive is used to mark a field as owned by another service.
This allows service A to use fields from service B while also knowing at runtime the types of that field.
"""
directive :external do
on [:field_definition]
end

@desc """
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.
"""
directive :requires do
arg :fields, non_null(:_field_set)
on [:field_definition]
end

@desc """
The `@provides` directive is used to annotate the expected returned fieldset
from a field on a base type that is guaranteed to be selectable by the gateway.
"""
directive :provides do
arg :fields, non_null(:_field_set)
on [:field_definition]
end

directive :extends do
on [:object, :interface]
end

@desc """
The `@link` directive links definitions within the document to external schemas.
"""
directive :link do
arg :url, :string
arg :as, :string
arg :for, :link_purpose
arg :import, :link_import
repeatable true
on [:schema]
end

@desc """
The `@shareable` directive is used to indicate that a field can be resolved by multiple subgraphs.
Any subgraph that includes a shareable field can potentially resolve a query for that field.
To successfully compose, a field must have the same shareability mode (either shareable or non-shareable)
across all subgraphs.
"""
directive :shareable do
on [:field_definition, :object]
end

@desc """
The `@override` directive is used to indicate that the current subgraph is
taking responsibility for resolving the marked field away from
the subgraph specified in the from argument.
"""
directive :override do
arg :from, non_null(:string)

on [:field_definition]
end

@desc """
The `@inaccessible` directive indicates that a field or type should be omitted from the gateway's API schema,
even if the field is also defined in other subgraphs.
"""
directive :inaccessible do
on [
:field_definition,
:object,
:interface,
:union,
:argument_definition,
:scalar,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
end

@desc """
The `@tag` directive indicates whether to include or exclude the field/type from your contract schema.
"""
directive :tag do
arg :name, non_null(:string)

repeatable true

on [
:field_definition,
:object,
:interface,
:union,
:argument_definition,
:scalar,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
end
use Absinthe.Federation.Schema.Prototype.FederatedDirectives
end
146 changes: 146 additions & 0 deletions lib/absinthe/federation/schema/prototype/federated_directives.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
defmodule Absinthe.Federation.Schema.Prototype.FederatedDirectives do
@moduledoc false

use Absinthe.Schema.Notation

defmacro __using__(_) do
quote do
@desc """
_FieldSet is a custom scalar type that is used to represent a set of fields.
Grammatically, a field set is a selection set minus the braces.
This means it can represent a single field "upc", multiple fields "id countryCode",
and even nested selection sets "id organization { id }"
"""
scalar :_field_set, name: "_FieldSet" do
serialize & &1
parse &{:ok, &1}
end

enum :link_purpose, name: "link__Purpose" do
value :security
value :execution
end

scalar :link_import, name: "link__Import" do
serialize & &1
parse &{:ok, &1}
end

@desc """
The `@key` directive is used to indicate a combination of fields that can be used
to uniquely identify and fetch an object or interface.
"""
directive :key do
arg :fields, non_null(:_field_set)
arg :resolvable, :boolean, default_value: true
repeatable true
on [:object, :interface]
end

@desc """
The @external directive is used to mark a field as owned by another service.
This allows service A to use fields from service B while also knowing at runtime the types of that field.
"""
directive :external do
on [:field_definition]
end

@desc """
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.
"""
directive :requires do
arg :fields, non_null(:_field_set)
on [:field_definition]
end

@desc """
The `@provides` directive is used to annotate the expected returned fieldset
from a field on a base type that is guaranteed to be selectable by the gateway.
"""
directive :provides do
arg :fields, non_null(:_field_set)
on [:field_definition]
end

directive :extends do
on [:object, :interface]
end

@desc """
The `@link` directive links definitions within the document to external schemas.
"""
directive :link do
arg :url, :string
arg :as, :string
arg :for, :link_purpose
arg :import, :link_import
repeatable true
on [:schema]
end

@desc """
The `@shareable` directive is used to indicate that a field can be resolved by multiple subgraphs.
Any subgraph that includes a shareable field can potentially resolve a query for that field.
To successfully compose, a field must have the same shareability mode (either shareable or non-shareable)
across all subgraphs.
"""
directive :shareable do
on [:field_definition, :object]
end

@desc """
The `@override` directive is used to indicate that the current subgraph is
taking responsibility for resolving the marked field away from
the subgraph specified in the from argument.
"""
directive :override do
arg :from, non_null(:string)

on [:field_definition]
end

@desc """
The `@inaccessible` directive indicates that a field or type should be omitted from the gateway's API schema,
even if the field is also defined in other subgraphs.
"""
directive :inaccessible do
on [
:field_definition,
:object,
:interface,
:union,
:argument_definition,
:scalar,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
end

@desc """
The `@tag` directive indicates whether to include or exclude the field/type from your contract schema.
"""
directive :tag do
arg :name, non_null(:string)

repeatable true

on [
:field_definition,
:object,
:interface,
:union,
:argument_definition,
:scalar,
:enum,
:enum_value,
:input_object,
:input_field_definition
]
end
end
end
end
9 changes: 9 additions & 0 deletions lib/absinthe/federation/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ defmodule Absinthe.Federation.Types do
serialize fn value -> value end
end

@desc """
Schema composition at the gateway requires having each service's schema, annotated with its federation configuration.
This information is fetched from each service using _service, an enhanced introspection entry point added to the
query root of each federated service.
"""
object :service, name: "_Service" do
@desc """
This SDL (schema definition language) is a printed version of the service's schema including the annotations of
federation directives. This SDL does not include the additions of the federation spec.
"""
field :sdl, :string
end
end
33 changes: 33 additions & 0 deletions test/absinthe/federation/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,37 @@ defmodule Absinthe.Federation.SchemaTest do
refute sdl =~ "_entities(representations: [_Any!]!): [_Entity]!"
end
end

describe "schema with custom prototype" do
defmodule CustomPrototype do
use Absinthe.Schema.Prototype
use Absinthe.Federation.Schema.Prototype.FederatedDirectives

directive :my_directive do
on [:schema]
end
end

defmodule CustomPrototypeSchema do
use Absinthe.Schema
use Absinthe.Federation.Schema, skip_prototype: true

@prototype_schema CustomPrototype

extend schema do
directive :link, url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]
directive :myDirective
end

query do
field :hello, :string
end
end

test "it includes federation and custom directions" do
sdl = Absinthe.Federation.to_federated_sdl(CustomPrototypeSchema)

assert sdl =~ "schema @myDirective @link"
end
end
end

0 comments on commit ef3ca32

Please sign in to comment.