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

Introduce Lookup Directive. #30

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
190 changes: 116 additions & 74 deletions spec/Section 2 -- Source Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,177 @@

## Directives

### @entityResolver
### @lookup

```graphql
directive @entityResolver on FIELD_DEFINITION
directive @lookup(map: SelectionPath!) on FIELD_DEFINITION
```

Entity resolvers are fields on the query root type of a subgraph that can
resolve an entity by a stable key. The stable key is defined by the arguments of
the field.
The `@lookup` directive is used within a _source schema_ to specify object
fields that can be used by the _distributed GraphQL executor_ to resolve an
entity by a stable key.

The stable key is defined by the arguments of the field. Only fields that are
annotated with the `@lookup` directive will be recognized as lookup field.

Source schemas can provide multiple lookup fields for the same entity with
different keys.

In this example the source schema specifies that the `Person` entity can be
resolved with the `personById` field or the `personByName` field on the `Query`
type. Both fields can resolve the same entity but do so with different keys.

```graphql example
extend type Query {
version: Int # NOT an entity resolver.
personById(id: ID!): Person @entityResolver
type Query {
version: Int # NOT a lookup field.
personById(id: ID!): Person @lookup(map: "{ id: id }")
personByName(name: String!): Person @lookup(map: "{ name: name }")
}

extend type Person {
id: ID! # matches the argument of personById
type Person @key(fields "id") @key(fields "name") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type Person @key(fields "id") @key(fields "name") {
type Person @key(fields: "id") @key(fields: "name") {

id: ID!
name: String!
}
```

The arguments of an entity resolver field must match fields of the returning
type.
The selection syntax provided as a value to the `map` argument of the `@lookup`
directive must correspond to the all arguments of a lookup field. Further it
must correspond to fields specified by a `@key` directive annotated on the
return type of the lookup field.

```graphql example
extend type Query {
node(id: ID!): Node @entityResolver
type Query {
node(id: ID!): Node @lookup(map: "{ id: id }")
}

interface Node {
interface Node @key(fields "id") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface Node @key(fields "id") {
interface Node @key(fields: "id") {

id: ID!
}
```

When an entity resolver returns an interface all implementing types are inferred
as entities.
Lookup fields may return object, interface or union types. In case a lookup
field returns an interface or union type all possible object types are
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
considered entities and must have keys that correspond with the lookup map.

```graphql example
extend type Query {
entityById(id: ID!, categoryId: Int): Entity @entityResolver
type Query {
entityById(id: ID!, categoryId: Int): Entity @lookup(map: "{ id: id, categoryId: categoryId }")
}

union Entity = Cat | Dog

extend type Dog {
type Dog @key(fields "id categoryId") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type Dog @key(fields "id categoryId") {
type Dog @key(fields: "id categoryId") {

id: ID!
categoryId: Int
}

extend type Cat {
type Cat @key(fields "id categoryId") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type Cat @key(fields "id categoryId") {
type Cat @key(fields: "id categoryId") {

id: ID!
categoryId: Int
}
```

### @is
The following example shows an invalid lookup field as the `Cat` type does not
declare a key that corresponds with the lookup fields argument signature.

```graphql
directive @is(
field: FieldSelection
coordinate: Schemacoordinate
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
```
```graphql counter-example
type Query {
entityById(id: ID!, categoryId: Int): Entity @lookup(map: "{ id: id, categoryId: categoryId }")
}

The `@is` directive is utilized to establish semantic equivalence between
disparate type system members across distinct subgraphs, which the schema
composition uses to connect types.
union Entity = Cat | Dog

In the following example, the directive specifies that the `id` argument on the
field `Query.personById` and the field `Person.id` on the return type of the
field are semantically the same. This information is used to infer an entity
resolver for `Person` from the field `Query.personById`.
type Dog @key(fields "id categoryId") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type Dog @key(fields "id categoryId") {
type Dog @key(fields: "id categoryId") {

id: ID!
categoryId: Int
}

```graphql example
extend type Query {
personById(id: ID! @is(field: "id")): Person @entityResolver
type Cat @key(fields "id") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type Cat @key(fields "id") {
type Cat @key(fields: "id") {

id: ID!
}
```

The `@is` directive also allows to refer to nested fields relative to `Person`.
If the lookup returns an interface in particular, the interface must also be
annotated with a `@key` directive.

```graphql example
extend type Query {
personByAddressId(id: ID! @is(field: "address { id }")): Person
interface Node @key(fields "id") {
id: ID!
}
```

The `@is` directive not limited to a single argument.
Lookup fields must be accessible from the Query type. If not directly on the
Query type, they must be accessible via fields that do not require arguments,
starting from the Query root type.

```graphql example
extend type Query {
personByAddressId(
id: ID! @is(field: "address { id }")
kind: PersonKind @is(field: "kind")
): Person
type Query {
lookups: Lookups!
}

type Lookups {
personById(id: ID!): Person @lookup(map: "{ id: id }")
}

type Person @key(fields "id") {
id: ID!
}
```

The directive can also establish semantic equivalence between two output fields.
In this example, the field `productSKU` is semantically equivalent to the field
`Product.sku`, allowing the schema composition to infer the connection of the
`Product` with the `Review` type.
Lookup fields can also use the `@oneOf` directive to specify a lookup field that
can resolve multiple keys.

```graphql example
extend type Review {
productSKU: ID! @is(coordinate: "Product.sku") @internal
product: Product @resolve
type Query {
person(finder: PersonFinderInput!): Person @lookup(map: "{ name: name } | { id: id }")
}

type Person @key(fields "id") @key(fields "name") {
id: ID!
name: String!
}

input PersonFinderInput @oneOf {
id: ID
name: String
}
```

The `@is` directive can use either the `field` or `coordinate` argument. If both
are specified, the schema composition must fail.
**Arguments:**

```graphql counter-example
extend type Review {
productSKU: ID!
@is(coordinate: "Product.sku", field: "product { sku }")
@internal
product: Product @resolve
- `map`: Represents a selection path that describes how keys are mapped to
arguments of a lookup field.

### @patch

```graphql
directive @patch(map: SelectionPath!) on FIELD_DEFINITION
```

The `@patch` directive is used within a _source schema_ to specify object fields
that can be used by the _distributed GraphQL executor_ to resolve additional
data for an entity rather than fetching the entity itself. A patch resolver
result does noth mean that the actual entity exists.

```graphql example
type Query {
personById(id: ID!): Person @patch(map: "{ id: id }")
personByName(name: String!): Person @patch(map: "{ name: name }")
}

type Person @key(fields "id") @key(fields "name") {
id: ID!
name: String!
}
```

Patch resolve as oposed to lookup fields will be omitted from the _composite
schema_ but will be referenced within the _composite execution schema_.

**Arguments:**

- `field`: Represents a GraphQL field selection syntax that refers to field
relative to the current type; or, when used on arguments it refers to a field
relative to the return type.
- `coordinate`: Represents a schema coordinate that refers to a type system
member.
- `map`: Represents a selection path that describes how keys are mapped to
arguments of a lookup field.

### @shareable

Expand All @@ -156,8 +198,8 @@ Note: Key fields are always considered sharable.

```graphql
directive @require(
field: FieldSelection!
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
map: SelectionPath!
) on FIELD_DEFINITION
```

The `@require` directive is used to express data requirements with other
Expand All @@ -170,9 +212,9 @@ type Product {
id: ID!
delivery(
zip: String!
size: Int! @require(field: "dimension { size }")
weight: Int! @require(field: "dimension { weight }")
): DeliveryEstimates
size: Int!
weight: Int!
): DeliveryEstimates @require(field: "{ size: dimension.size weight: dimension.weight }")
}
```

Expand All @@ -185,8 +227,8 @@ type Product {
id: ID!
delivery(
zip: String!
dimension: ProductDimensionInput! @require(field: "dimension"))
): DeliveryEstimates
dimension: ProductDimensionInput!)
): DeliveryEstimates @require(field: "{ dimension: { size: dimension.size weight: dimension.weight } }")
}
```

Expand Down