|
19 | 19 | - [Filtering](#filtering) |
20 | 20 | - [Ordering](#ordering) |
21 | 21 | - [Pagination](#pagination) |
| 22 | + - [Cursor pagination](#cursor-pagination) |
22 | 23 | - [Permanent filters](#permanent-filters) |
23 | 24 |
|
24 | 25 | ## Description |
@@ -500,6 +501,123 @@ This will add arguments to the query for pagination: |
500 | 501 |
|
501 | 502 | When working with pagination, it is important to remember [point 5 of the important section](#important). |
502 | 503 |
|
| 504 | +## [Cursor pagination](https://the-guild.dev/blog/graphql-cursor-pagination-with-postgresql) |
| 505 | + |
| 506 | +Pagination works in tandem with a data loader, filters, and sorting and allows you to limit the number of records received from the database |
| 507 | + |
| 508 | +```ts |
| 509 | +@Resolver(() => Author) |
| 510 | +export class AuthorResolver { |
| 511 | + ... |
| 512 | + @Query(() => [Author]) |
| 513 | + public async authors( |
| 514 | + @Loader({ |
| 515 | + loader_type: ELoaderType.MANY, |
| 516 | + field_name: 'authors', |
| 517 | + entity: () => Author, |
| 518 | + entity_fk_key: 'id', |
| 519 | + }) field_alias: string, |
| 520 | + @Filter(() => Author) _filter: unknown, // <-- ADD |
| 521 | + @Order(() => Author) _order: unknown, // <-- ADD |
| 522 | + @Pagination() _pagination: unknown, // <-- ADD |
| 523 | + @Context() ctx: GraphQLExecutionContext |
| 524 | + ) { |
| 525 | + return await ctx[field_alias]; |
| 526 | + } |
| 527 | + ... |
| 528 | +} |
| 529 | +``` |
| 530 | + |
| 531 | +Then you can get the first page using the query: |
| 532 | + |
| 533 | +```gql |
| 534 | +query firstPage { |
| 535 | + authors( |
| 536 | + ORDER: { id: { SORT: ASC } } |
| 537 | + PAGINATION: { per_page: 10 } |
| 538 | + ) { |
| 539 | + id |
| 540 | + } |
| 541 | +} |
| 542 | +``` |
| 543 | + |
| 544 | +Then you can get the next page using the query: |
| 545 | + |
| 546 | +```gql |
| 547 | +query nextPage($ID_of_the_last_element_from_the_previous_page: ID!) { |
| 548 | + authors( |
| 549 | + WHERE: { id: { GT: $ID_of_the_last_element_from_the_previous_page }} |
| 550 | + ORDER: { id: { SORT: ASC } } |
| 551 | + PAGINATION: { per_page: 10 } |
| 552 | + ) { |
| 553 | + id |
| 554 | + } |
| 555 | +} |
| 556 | +``` |
| 557 | + |
| 558 | +Fields that are planned to be used as a cursor must be allowed for filtering and sorting in the `@Field` decorator, and it is also recommended to index them indicating the sort order. |
| 559 | + |
| 560 | +With such pagination, it is important to take into account the order in which the fields specified in the sorting are listed. |
| 561 | + |
| 562 | +You can also use several fields as cursors. The main thing is to maintain order. |
| 563 | + |
| 564 | +Then you can get the first page using the query: |
| 565 | + |
| 566 | +```gql |
| 567 | +query firstPage{ |
| 568 | + authors( |
| 569 | + ORDER: { updated_at: { SORT: DESC }, id: { SORT: ASC } } |
| 570 | + PAGINATION: { per_page: 10 } |
| 571 | + ) { |
| 572 | + id |
| 573 | + } |
| 574 | +} |
| 575 | + |
| 576 | +``` |
| 577 | + |
| 578 | +Then you can get the next page using the query: |
| 579 | + |
| 580 | +```gql |
| 581 | +query nextPage( |
| 582 | + $UPDATED_AT_of_the_last_element_from_the_previous_page: DateTime! |
| 583 | + $ID_of_the_last_element_from_the_previous_page: ID! |
| 584 | +) { |
| 585 | + authors( |
| 586 | + WHERE: { |
| 587 | + updated_at: { LT: $UPDATED_AT_of_the_last_element_from_the_previous_page } |
| 588 | + OR: { |
| 589 | + updated_at: { |
| 590 | + EQ: $UPDATED_AT_of_the_last_element_from_the_previous_page |
| 591 | + } |
| 592 | + id: { GT: $ID_of_the_last_element_from_the_previous_page } |
| 593 | + } |
| 594 | + } |
| 595 | + ORDER: { updated_at: { SORT: DESC }, id: { SORT: ASC } } |
| 596 | + PAGINATION: { per_page: 10 } |
| 597 | + ) { |
| 598 | + id |
| 599 | + } |
| 600 | +} |
| 601 | +``` |
| 602 | + |
| 603 | +However, it is recommended to limit the time columns to milliseconds: |
| 604 | + |
| 605 | +```ts |
| 606 | +@ObjectType() |
| 607 | +@Entity() |
| 608 | +export class Author { |
| 609 | + ... |
| 610 | + @Field(() => Date, { filterable: true, sortable: true }) |
| 611 | + @UpdateDateColumn({ |
| 612 | + type: 'timestamp without time zone', |
| 613 | + precision: 3, // <-- ADD |
| 614 | + default: () => 'CURRENT_TIMESTAMP', |
| 615 | + }) |
| 616 | + public updated_at: Date; |
| 617 | + ... |
| 618 | +} |
| 619 | +``` |
| 620 | + |
503 | 621 | ## Permanent filters |
504 | 622 |
|
505 | 623 | You can also specify permanent filters that will always be applied regardless of the query |
|
0 commit comments