0
# Relay Specification
1
2
Complete Relay specification implementation with Node interface, connections, pagination, and global object identification. The Relay specification provides standards for cursor-based pagination, global object identification, and mutations.
3
4
## Capabilities
5
6
### Node Interface
7
8
Global object identification following the Relay Node interface pattern.
9
10
```python { .api }
11
class Node:
12
"""Relay Node interface for global object identification."""
13
14
id: NodeID
15
16
class NodeID:
17
"""Node ID type with automatic encoding/decoding."""
18
19
def __init__(self, node_id: str, type_name: str = None): ...
20
21
class GlobalID:
22
"""Global ID implementation with type and ID encoding."""
23
24
def __init__(self, type_name: str, node_id: str): ...
25
26
@classmethod
27
def from_id(cls, global_id: str) -> "GlobalID": ...
28
29
def to_id(self) -> str: ...
30
31
class GlobalIDValueError(ValueError):
32
"""Exception raised when GlobalID parsing fails."""
33
```
34
35
**Usage Example:**
36
37
```python
38
@strawberry.type
39
class User(strawberry.relay.Node):
40
id: strawberry.relay.NodeID
41
name: str
42
email: str
43
44
@classmethod
45
def resolve_node(cls, node_id: str, info: strawberry.Info):
46
"""Resolve node from global ID."""
47
user_data = get_user_by_id(node_id)
48
return cls(
49
id=strawberry.relay.NodeID(node_id, "User"),
50
name=user_data["name"],
51
email=user_data["email"]
52
)
53
54
@strawberry.type
55
class Post(strawberry.relay.Node):
56
id: strawberry.relay.NodeID
57
title: str
58
content: str
59
60
@classmethod
61
def resolve_node(cls, node_id: str, info: strawberry.Info):
62
post_data = get_post_by_id(node_id)
63
return cls(
64
id=strawberry.relay.NodeID(node_id, "Post"),
65
title=post_data["title"],
66
content=post_data["content"]
67
)
68
69
@strawberry.type
70
class Query:
71
# Relay requires a node field on Query
72
node: strawberry.relay.Node = strawberry.relay.node()
73
```
74
75
### Node Decorator
76
77
Decorator to mark types as Relay nodes with automatic node resolution.
78
79
```python { .api }
80
def node(
81
cls=None,
82
*,
83
node_resolver: Callable = None
84
) -> Any:
85
"""
86
Decorator to mark types as Relay nodes.
87
88
Args:
89
cls: The class to mark as a node
90
node_resolver: Custom node resolver function
91
92
Returns:
93
Node-enabled GraphQL type
94
"""
95
```
96
97
**Usage Example:**
98
99
```python
100
@strawberry.relay.node
101
@strawberry.type
102
class User:
103
id: strawberry.relay.NodeID
104
name: str
105
email: str
106
107
# The node decorator automatically adds node resolution
108
@strawberry.type
109
class Query:
110
node: strawberry.relay.Node = strawberry.relay.node()
111
112
@strawberry.field
113
def user(self, id: strawberry.ID) -> User:
114
return User.resolve_node(id, info)
115
```
116
117
### Connections and Pagination
118
119
Cursor-based pagination following the Relay Connection specification.
120
121
```python { .api }
122
class Connection:
123
"""Relay connection interface for paginated results."""
124
125
edges: List[Edge]
126
page_info: PageInfo
127
128
class Edge:
129
"""Connection edge containing a node and cursor."""
130
131
node: Node
132
cursor: str
133
134
class PageInfo:
135
"""Pagination information for connections."""
136
137
has_previous_page: bool
138
has_next_page: bool
139
start_cursor: Optional[str]
140
end_cursor: Optional[str]
141
142
class ListConnection:
143
"""List-based connection implementation."""
144
145
@classmethod
146
def resolve_connection(
147
cls,
148
nodes: List[Any],
149
*,
150
first: int = None,
151
after: str = None,
152
last: int = None,
153
before: str = None
154
) -> Connection: ...
155
```
156
157
**Usage Examples:**
158
159
```python
160
@strawberry.type
161
class UserConnection(strawberry.relay.Connection[User]):
162
"""Connection for User nodes."""
163
164
edges: List[strawberry.relay.Edge[User]]
165
page_info: strawberry.relay.PageInfo
166
167
@strawberry.type
168
class UserEdge(strawberry.relay.Edge[User]):
169
"""Edge for User nodes."""
170
171
node: User
172
cursor: str
173
174
@strawberry.type
175
class Query:
176
@strawberry.field
177
def users(
178
self,
179
first: int = None,
180
after: str = None,
181
last: int = None,
182
before: str = None
183
) -> UserConnection:
184
# Get all users (in practice, this would be a database query)
185
all_users = get_all_users()
186
187
# Convert to User objects
188
user_nodes = [
189
User(
190
id=strawberry.relay.NodeID(str(u.id), "User"),
191
name=u.name,
192
email=u.email
193
)
194
for u in all_users
195
]
196
197
# Use ListConnection for automatic pagination
198
return strawberry.relay.ListConnection.resolve_connection(
199
user_nodes,
200
first=first,
201
after=after,
202
last=last,
203
before=before
204
)
205
```
206
207
### Connection Decorator
208
209
Decorator to create connection fields with automatic pagination.
210
211
```python { .api }
212
def connection(
213
resolver: Callable = None,
214
*,
215
name: str = None,
216
description: str = None,
217
permission_classes: List[Type[BasePermission]] = None,
218
extensions: List[FieldExtension] = None
219
) -> Any:
220
"""
221
Decorator to create Relay connection fields.
222
223
Args:
224
resolver: Resolver function that returns nodes
225
name: Custom field name
226
description: Field description
227
permission_classes: Permission classes for authorization
228
extensions: Field extensions to apply
229
230
Returns:
231
Connection field with automatic pagination
232
"""
233
```
234
235
**Usage Example:**
236
237
```python
238
@strawberry.type
239
class User:
240
id: strawberry.relay.NodeID
241
name: str
242
243
@strawberry.relay.connection(
244
description="User's posts with cursor-based pagination"
245
)
246
def posts(
247
self,
248
first: int = None,
249
after: str = None,
250
last: int = None,
251
before: str = None
252
) -> List[Post]:
253
# Return list of Post objects
254
# Connection decorator handles pagination automatically
255
return get_posts_by_user_id(self.id)
256
257
@strawberry.type
258
class Query:
259
@strawberry.relay.connection
260
def all_users(
261
self,
262
first: int = None,
263
after: str = None,
264
last: int = None,
265
before: str = None,
266
name_filter: str = None
267
) -> List[User]:
268
return get_users(name_filter=name_filter)
269
```
270
271
### Cursor Utilities
272
273
Utilities for encoding and decoding cursors.
274
275
```python { .api }
276
def to_base64(value: str) -> str:
277
"""
278
Encode string to base64 for cursor encoding.
279
280
Args:
281
value: String value to encode
282
283
Returns:
284
Base64 encoded string
285
"""
286
287
def from_base64(cursor: str) -> str:
288
"""
289
Decode base64 cursor back to string.
290
291
Args:
292
cursor: Base64 encoded cursor
293
294
Returns:
295
Decoded string value
296
"""
297
```
298
299
**Usage Example:**
300
301
```python
302
from strawberry.relay import to_base64, from_base64
303
304
# Custom cursor creation
305
def create_cursor(post_id: str, created_at: datetime) -> str:
306
cursor_data = f"{post_id}:{created_at.isoformat()}"
307
return to_base64(cursor_data)
308
309
def parse_cursor(cursor: str) -> tuple[str, datetime]:
310
cursor_data = from_base64(cursor)
311
post_id, created_at_str = cursor_data.split(":", 1)
312
created_at = datetime.fromisoformat(created_at_str)
313
return post_id, created_at
314
315
@strawberry.type
316
class Post:
317
id: strawberry.relay.NodeID
318
title: str
319
created_at: datetime
320
321
def get_cursor(self) -> str:
322
return create_cursor(str(self.id), self.created_at)
323
```
324
325
## Advanced Connection Patterns
326
327
### Custom Connection Implementation
328
329
```python
330
@strawberry.type
331
class PostConnection:
332
edges: List[strawberry.relay.Edge[Post]]
333
page_info: strawberry.relay.PageInfo
334
total_count: int # Additional field not in standard Connection
335
336
@classmethod
337
def from_posts(
338
cls,
339
posts: List[Post],
340
first: int = None,
341
after: str = None,
342
last: int = None,
343
before: str = None,
344
total_count: int = None
345
) -> "PostConnection":
346
# Custom pagination logic
347
start_index = 0
348
end_index = len(posts)
349
350
if after:
351
try:
352
after_id, _ = parse_cursor(after)
353
start_index = next(
354
i for i, post in enumerate(posts)
355
if str(post.id) == after_id
356
) + 1
357
except (ValueError, StopIteration):
358
start_index = 0
359
360
if before:
361
try:
362
before_id, _ = parse_cursor(before)
363
end_index = next(
364
i for i, post in enumerate(posts)
365
if str(post.id) == before_id
366
)
367
except (ValueError, StopIteration):
368
end_index = len(posts)
369
370
if first is not None:
371
end_index = min(end_index, start_index + first)
372
373
if last is not None:
374
start_index = max(start_index, end_index - last)
375
376
selected_posts = posts[start_index:end_index]
377
378
edges = [
379
strawberry.relay.Edge(
380
node=post,
381
cursor=post.get_cursor()
382
)
383
for post in selected_posts
384
]
385
386
page_info = strawberry.relay.PageInfo(
387
has_previous_page=start_index > 0,
388
has_next_page=end_index < len(posts),
389
start_cursor=edges[0].cursor if edges else None,
390
end_cursor=edges[-1].cursor if edges else None
391
)
392
393
return cls(
394
edges=edges,
395
page_info=page_info,
396
total_count=total_count or len(posts)
397
)
398
399
@strawberry.type
400
class Query:
401
@strawberry.field
402
def posts(
403
self,
404
first: int = None,
405
after: str = None,
406
last: int = None,
407
before: str = None,
408
category: str = None
409
) -> PostConnection:
410
# Get posts with filtering
411
posts = get_posts(category=category)
412
total_count = get_posts_count(category=category)
413
414
return PostConnection.from_posts(
415
posts=posts,
416
first=first,
417
after=after,
418
last=last,
419
before=before,
420
total_count=total_count
421
)
422
```
423
424
### Nested Connections
425
426
```python
427
@strawberry.type
428
class User(strawberry.relay.Node):
429
id: strawberry.relay.NodeID
430
name: str
431
432
@strawberry.relay.connection
433
def posts(
434
self,
435
first: int = None,
436
after: str = None,
437
status: str = "published"
438
) -> List[Post]:
439
return get_user_posts(
440
user_id=self.id,
441
status=status
442
)
443
444
@strawberry.relay.connection
445
def followers(
446
self,
447
first: int = None,
448
after: str = None
449
) -> List["User"]:
450
return get_user_followers(self.id)
451
452
@strawberry.type
453
class Post(strawberry.relay.Node):
454
id: strawberry.relay.NodeID
455
title: str
456
author: User
457
458
@strawberry.relay.connection
459
def comments(
460
self,
461
first: int = None,
462
after: str = None
463
) -> List[Comment]:
464
return get_post_comments(self.id)
465
```
466
467
### Connection with Filtering and Sorting
468
469
```python
470
@strawberry.enum
471
class PostSortOrder(Enum):
472
CREATED_ASC = "created_asc"
473
CREATED_DESC = "created_desc"
474
TITLE_ASC = "title_asc"
475
TITLE_DESC = "title_desc"
476
477
@strawberry.input
478
class PostFilter:
479
category: Optional[str] = None
480
published: Optional[bool] = None
481
author_id: Optional[strawberry.ID] = None
482
483
@strawberry.type
484
class Query:
485
@strawberry.relay.connection
486
def posts(
487
self,
488
first: int = None,
489
after: str = None,
490
last: int = None,
491
before: str = None,
492
filter: PostFilter = None,
493
sort_by: PostSortOrder = PostSortOrder.CREATED_DESC
494
) -> List[Post]:
495
return get_posts_with_filter_and_sort(
496
filter=filter,
497
sort_by=sort_by
498
)
499
```
500
501
## Extensions for Relay
502
503
### Node Extension
504
505
Extension for automatic node resolution.
506
507
```python { .api }
508
class NodeExtension(FieldExtension):
509
"""Extension for Relay node resolution."""
510
511
def apply(self, field: StrawberryField) -> StrawberryField: ...
512
```
513
514
### Connection Extension
515
516
Extension for automatic connection resolution.
517
518
```python { .api }
519
class ConnectionExtension(FieldExtension):
520
"""Extension for Relay connection resolution."""
521
522
def apply(self, field: StrawberryField) -> StrawberryField: ...
523
```
524
525
## Relay Mutations
526
527
Relay mutation pattern with input and payload types:
528
529
```python
530
@strawberry.input
531
class CreatePostInput:
532
title: str
533
content: str
534
category_id: strawberry.ID
535
536
@strawberry.type
537
class CreatePostPayload:
538
post: Optional[Post]
539
user_error: Optional[str]
540
client_mutation_id: Optional[str]
541
542
@strawberry.type
543
class Mutation:
544
@strawberry.mutation
545
def create_post(
546
self,
547
input: CreatePostInput,
548
client_mutation_id: str = None
549
) -> CreatePostPayload:
550
try:
551
post = create_new_post(
552
title=input.title,
553
content=input.content,
554
category_id=input.category_id
555
)
556
return CreatePostPayload(
557
post=post,
558
user_error=None,
559
client_mutation_id=client_mutation_id
560
)
561
except ValidationError as e:
562
return CreatePostPayload(
563
post=None,
564
user_error=str(e),
565
client_mutation_id=client_mutation_id
566
)
567
```
568
569
## Complete Relay Example
570
571
```python
572
import strawberry
573
from strawberry import relay
574
from typing import List, Optional
575
576
@strawberry.relay.node
577
@strawberry.type
578
class User:
579
id: strawberry.relay.NodeID
580
name: str
581
email: str
582
583
@strawberry.relay.connection
584
def posts(self) -> List["Post"]:
585
return get_user_posts(self.id)
586
587
@strawberry.relay.node
588
@strawberry.type
589
class Post:
590
id: strawberry.relay.NodeID
591
title: str
592
content: str
593
author_id: strawberry.ID
594
595
@strawberry.field
596
def author(self) -> User:
597
return User.resolve_node(self.author_id, info)
598
599
@strawberry.type
600
class Query:
601
# Required node field for Relay
602
node: strawberry.relay.Node = strawberry.relay.node()
603
604
@strawberry.relay.connection
605
def users(self) -> List[User]:
606
return get_all_users()
607
608
@strawberry.relay.connection
609
def posts(self, category: str = None) -> List[Post]:
610
return get_posts(category=category)
611
612
schema = strawberry.Schema(query=Query)
613
```
614
615
**Example Query:**
616
```graphql
617
query RelayQuery {
618
# Node query
619
node(id: "VXNlcjox") {
620
id
621
... on User {
622
name
623
624
}
625
}
626
627
# Connection query
628
users(first: 5, after: "cursor") {
629
edges {
630
node {
631
id
632
name
633
posts(first: 3) {
634
edges {
635
node {
636
title
637
}
638
}
639
}
640
}
641
cursor
642
}
643
pageInfo {
644
hasNextPage
645
hasPreviousPage
646
startCursor
647
endCursor
648
}
649
}
650
}
651
```