or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-types.mdexperimental.mdextensions.mdfederation.mdfields-resolvers.mdframework-integrations.mdindex.mdrelay.mdschema-execution.mdutilities.md

relay.mddocs/

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

email

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

```