Subchapter 2.1
references/entities.mdMarkdown7 KBView on GitHub
An entity is a type that can be resolved by a unique key. In connectors, add @connect to a type to make it an entity.
# This makes User an entity - no @key needed
type User @connect(
source: "api"
http: { GET: "/users/{$this.id}" }
selection: "id name email"
) {
id: ID!
name: String
email: String
}Key rules:
@key directive - @connect on type is sufficient@connect on it@connect, no @key)When a parent type returns an ID that references an entity, create an entity stub in the selection.
API Response:
{
"id": "order-1",
"userId": "user-123",
"total": 99.99
}Schema:
type Order {
id: ID!
user: User # Entity reference
total: Float
}
type User @connect(
source: "api"
http: { GET: "/users/{$this.id}" }
selection: "id name email"
) {
id: ID!
name: String
email: String
}
type Query {
order(id: ID!): Order
@connect(
source: "api"
http: { GET: "/orders/{$args.id}" }
selection: """
id
user: { id: userId } # Entity stub - only the key field
total
"""
)
}The user: { id: userId } creates a stub with only the key field. The router then uses the User @connect to resolve the full user.
Use @connect on a type when you need:
$this for parent fields$batchtype Product @connect(
source: "api"
http: { GET: "/products/{$this.id}" }
selection: """
id
name
price
"""
) {
id: ID!
name: String
price: Float
}Use @connect on a field for simple parent-child relationships.
type User {
id: ID!
name: String
posts: [Post] @connect(
source: "api"
http: { GET: "/users/{$this.id}/posts" }
selection: "id title content"
)
}When to choose:
When an entity is defined in one subgraph and referenced in another:
Authoritative Subgraph (defines full entity):
type User @connect(
source: "users_api"
http: { GET: "/users/{$this.id}" }
selection: "id name email avatar"
) {
id: ID!
name: String
email: String
avatar: String
}Referencing Subgraph (entity stub only):
# Only define the key field
type User @key(fields: "id") {
id: ID!
}
type Order {
id: ID!
user: User # References the entity
}
type Query {
order(id: ID!): Order
@connect(
selection: """
id
user: { id: userId } # Create stub with key
"""
)
}Convert N+1 queries to batch requests using $batch.
Before (N+1 problem):
type Product @connect(
source: "api"
http: { GET: "/products/{$this.id}" }
selection: "id name price"
) {
id: ID!
name: String
price: Float
}After (batched):
type Product @connect(
source: "api"
http: {
POST: "/products/batch"
body: "ids: $batch.id"
}
selection: "id name price"
) {
id: ID!
name: String
price: Float
}$batch must be in the selection@connectbatch: { maxSize: N } to limit batch sizeWhen API returns grouped results:
API Response:
[
{ "productId": "1", "reviews": [...] },
{ "productId": "2", "reviews": [...] }
]Schema:
type Product @connect(
source: "api"
http: {
POST: "/reviews/batch"
body: "productIds: $batch.id"
}
selection: """
id: productId
reviews {
id
rating
text
}
"""
) {
id: ID!
reviews: [Review]
}Map the grouping key (productId) back to the entity key (id).
type Product @connect(
source: "api"
http: {
POST: "/products/batch"
body: "ids: $batch.id"
}
batch: { maxSize: 100 } # Limit to 100 items per request
selection: "id name"
) {
id: ID!
name: String
}When you encounter circular references, do NOT create entity stubs. Instead:
@inaccessible@connect back to the parenttype Product {
id: ID!
name: String
reviews: [Review] @connect(
source: "api"
http: { GET: "/products/{$this.id}/reviews" }
selection: "id rating text productId" # Include foreign key
)
}
type Review {
id: ID!
rating: Int
text: String
productId: ID! @inaccessible # Hidden from clients
product: Product @connect(
source: "api"
http: { GET: "/products/{$this.productId}" }
selection: "id name"
)
}Add multiple connectors when different endpoints provide different fields:
type User
@connect(
source: "api"
http: { GET: "/users/{$this.id}" }
selection: "id firstName lastName"
)
@connect(
source: "api"
http: { GET: "/users/{$this.id}?detailed=true" }
selection: """
id
address {
street
city
country
}
"""
) {
id: ID!
firstName: String
lastName: String
address: Address
}The router calls the appropriate connector(s) based on which fields are requested.
type User @connect(
http: { GET: "/users/{$this.id}" }
selection: "id name"
) {
id: ID!
name: String
}type User {
id: ID!
profile: Profile @connect(
http: { GET: "/users/{$this.id}/profile" }
selection: "bio avatar"
)
}type User @connect(
http: { POST: "/users/batch", body: "ids: $batch.id" }
selection: "id name"
) {
id: ID!
name: String
}# In orders subgraph
type User @key(fields: "id") {
id: ID! # Stub only
}
type Order {
user: User # Resolved by users subgraph
}