Skip to content

Commit

Permalink
Pagination (#144)
Browse files Browse the repository at this point in the history
* add paginate directive and transform

* inject page info into cursor-based pagination

* add validators for @paginate

* fixed pagination check

* rework internal api for looking up fragment arguments

* a required argument must be flagged with a !

* @paginate arg adds arguments to its fragment

* hoist first, last, and limit as fragment arguments

* hoist the pagination args as default values

* attempt to clean up pagination transform logic

* unname unused variable

* continue cleaning up pagination transform

* hoist correctly

* fix snapshot tests

* embed pagination fragment in a new query

* embed existing args as default values to query

* point snapshots at generated query

* fix tests

* dont rely on fragment argument transform for paginate tests

* fragment variables transform define fragments in new document

* add utils to verify node interface

* validate node interface and field

* paginating a fragment on node embeds it in a query on node

* paginate directive can only appear in a document once

* error if cursor pagination goes forwards and backwards

* @paginate cannot appear on a fragment with required args

* paginate from query

* paginated queries don't overlap existing variables

* more overlapping tests

* coordinate pagination transform with a central state object

* embed refetch meta data in artifact

* added tests for refetch spec

* tests pass

* invert generated flag - some generated documents need to be there

* add paginate directive and transform

* inject page info into cursor-based pagination

* add validators for @paginate

* fixed pagination check

* rework internal api for looking up fragment arguments

* a required argument must be flagged with a !

* @paginate arg adds arguments to its fragment

* hoist first, last, and limit as fragment arguments

* hoist the pagination args as default values

* attempt to clean up pagination transform logic

* unname unused variable

* continue cleaning up pagination transform

* hoist correctly

* fix snapshot tests

* embed pagination fragment in a new query

* embed existing args as default values to query

* point snapshots at generated query

* fix tests

* dont rely on fragment argument transform for paginate tests

* fragment variables transform define fragments in new document

* add utils to verify node interface

* validate node interface and field

* paginating a fragment on node embeds it in a query on node

* paginate directive can only appear in a document once

* error if cursor pagination goes forwards and backwards

* @paginate cannot appear on a fragment with required args

* paginate from query

* paginated queries don't overlap existing variables

* more overlapping tests

* coordinate pagination transform with a central state object

* embed refetch meta data in artifact

* added tests for refetch spec

* tests pass

* invert generated flag - some generated documents need to be there

* move pagination query name convention to config

* update example to use connections

* define paginatedQuery

* embed pagination behavior on appropriate field

* cache.write args are now an object

* rename selection.paginate to update

* failing tests for updating cache with pagination values

* cache can optionally merge results with existing values

* build artifact as object before serializing

* fix duplicate update field

* fix keys for embedded data

* write data before subscribing so subscribers dont get lost

* pass current page info as store

* @list tagged connections have the correct fragments generated

* lists on connections get flagged in their artifacts

* cache can append to connections

* @paginate can provide a list name

* failing test for removing record from connection

* can remove record from connection

* implement loadPreviousPage

* add list names and mutations back in

* add list item subscription back

* verify insert and deletes from connections

* update list location directives with schema-less api

* update cache test

* removed record list references

* make sure the store always has the fresh version when writing new data

* cleanup

* split up loadNext and loadPrevious functions

* start implementing offset pagination handler

* fix types in scalar tests

* first pass at offset pagination loader

* failing test for correctly loaded node in pagination result

* overwrite entries in a connection that come from list operations

* remove unreachable code

* add embedded flag to query refetch spec for fragments under node

* better test for embedded fragment queries

* first pass at paginatedFragment

* handle paginated componentQueries

* dont add __typename everywhere, just unions, interfaces, and connections

* only use typename for embedded references if it exists

* add preliminary documentation for pagination support

* v0.10.0-alpha.0

* fix pagination link in readme

* fix forward cursor-based example

* pass id to embedded fragment queries

* v0.10.0-alpha.1

* document name arg of paginate directive

* actually mix in query variables 🤦

* v0.10.0-alpha.2

* clarify that paginatedQueries do not need node interface/resolver

* typo in readme

* added missing fence to readme

* add paginated fragments and mutation operations to table of contents

* fix pageInfo example

* merge main

* merge conflict in code of conduct

* more readme tweaks

* undo CoC changes

* grammar is hard

* pass extra variables to offset pagination handler

* pass extra variables to loadNextPage

* bump

* more bumps

* v0.10.0-alpha.5

* connection targets might be non-null

* v0.10.0-alpha.8

* add loading state to pagination handlers

* v0.10.0-alpha.9

* document pagination loading state

* dry up cursor page loads

* more bumps

* v0.10.0-alpha.11

* catch empty page sizes

* mix query variables into pagination handlers

* v0.10.0-alpha.12

* context variables overwrite default ones for queries

* better check for missing page size

* typo

* v0.10.0-alpha.13
  • Loading branch information
AlecAivazis authored Jul 29, 2021
1 parent 74e0f69 commit a52397c
Show file tree
Hide file tree
Showing 70 changed files with 8,123 additions and 2,360 deletions.
32 changes: 16 additions & 16 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting

## Enforcement Responsibilities

Expand Down Expand Up @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
Expand Down
145 changes: 140 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ for the generation of an incredibly lean GraphQL abstraction for your applicatio
1. [Configuring the WebSocket client](#configuring-the-websocket-client)
1. [Using graphql-ws](#using-graphql-ws)
1. [Using subscriptions-transport-ws](#using-subscriptions-transport-ws)
1. [Pagination](#%EF%B8%8Fpagination)
1. [Paginated Fragments](#paginated-fragments)
1. [Mutation Operations](#mutation-operations)
1. [Custom Scalars](#%EF%B8%8Fcustom-scalars)
1. [Authentication](#authentication)
1. [Notes, Constraints, and Conventions](#%EF%B8%8Fnotes-constraints-and-conventions)
Expand Down Expand Up @@ -114,7 +117,7 @@ import houdini from 'houdini-preprocess'

### Sapper

You'll need to add the preprocessor to both your client and your server configuration. With that in place,
You'll need to add the preprocessor to both your client and your server configuration. With that in place,
the only thing left to configure your Sapper application is to connect your client and server to the generate network layer:

```typescript
Expand Down Expand Up @@ -444,9 +447,16 @@ fragment UserAvatar on User @arguments(width: {type:"Int", default: 50}) {
}
```

An argument with no default value is considered required. If no value is provided,
an error will be thrown when generating your runtime. Providing values for fragments
is done with the `@with` decorator:
In order to mark an argument as required, pass the type with a `!` at the end.
If no value is provided, an error will be thrown when generating your runtime.

```graphql
fragment UserAvatar on User @arguments(width: {type:"Int!"}) {
profilePicture(width: $width)
}
```

Providing values for fragments is done with the `@with` decorator:

```graphql
query AllUsers {
Expand Down Expand Up @@ -603,7 +613,7 @@ applied. To support this, houdini provides the `@when` and `@when_not` directive
```graphql
mutation NewItem($input: AddItemInput!) {
addItem(input: $input) {
...All_Items_insert @when_not(argument: "completed", value: "true")
...All_Items_insert @when_not(completed: true)
}
}
```
Expand Down Expand Up @@ -732,6 +742,131 @@ if (browser) {
export default new Environment(fetchQuery, socketClient)
```

## ♻️ Pagination

It's often the case that you want to avoid querying an entire list from your API in order
to minimize the amount of data transfers over the network. To support this, GraphQL APIs will
"paginate" a field, allowing users to query a slice of the list. The strategy used to access
slices of a list fall into two categories. Offset-based pagination relies `offset` and `limit`
arguments and mimics the mechanisms provided by most database engines. Cursor-based pagination
is a bi-directional strategy that relies on `first`/`after` or `last`/`before` arguments and
is designed to handle modern pagination features such a infinite scrolling.

Regardless of the strategy used, houdini follows a simple pattern: wrap your document in a
"paginated" function (ie, `paginatedQuery` or `paginatedFragment`), mark the field with
`@paginate`, and provide the "page size" via the `first`, `last` or `limit` arguments to the field.
`paginatedQuery` and `paginatedFragment` behave identically: they return a `data` field containing
a svelte store with your full dataset, functions you can call to load the next or previous
page, as well as a readable store with a boolean loading state. For example, a field
supporting offset-based pagination would look something like:

```javascript
const { data, loadNextPage, loading } = paginatedQuery(graphql`
query UserList {
friends(limit: 10) @paginate {
id
}
}
`)
```

and a field that supports cursor-based pagination starting at the end of the list would look something like:

```javascript
const { data, loadPreviousPage } = paginatedQuery(graphql`
query UserList {
friends(last: 10) @paginate {
edges {
node {
id
}
}
}
}
`)
```

If you are paginating a field with a cursor-based strategy (forward or backwards), the current page
info can be looked up with the `pageInfo` store returned from the paginated function:

```svelte
<script>
const { data, loadNextPage, pageInfo } = paginatedQuery(graphql`
query UserList {
friends(first: 10) @paginate {
edges {
node {
id
}
}
}
}
`)
</script>
{#if $pageInfo.hasNextPage}
<button onClick={() => loadNextPage()}> load more </button>
{/if}
```

### Paginated Fragments

`paginatedFragment` functions very similarly to `paginatedQuery` with a few caveats.
Consider the following:

```javascript
const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
fragment UserWithFriends on User {
friends(first: 10) @paginate {
edges {
node {
id
}
}
}
}
`)
```

In order to look up the next page for the user's friend. We need a way to query the specific user
that this fragment has been spread into. In order to pull this off, houdini relies on the generic `Node`
interface and corresponding query:

```graphql
interface Node {
id: ID!
}

type Query {
node(id: ID!): Node
}
```

In short, this means that any paginated fragment must be of a type that implements the Node interface
(so it can be looked up in the api). You can read more information about the `Node` interface in
[this section](https://graphql.org/learn/global-object-identification/) of the graphql community website.
This is only a requirement for paginated fragments. If your application only uses paginated queries,
you do not need to implement the Node interface and resolver.

### Mutation Operations

A paginated field can be marked as a potential target for a mutation operation by passing
a `name` argument to the `@paginate` directive:

```javascript
const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
fragment UserWithFriends on User {
friends(first: 10) @paginate(name: "User_Friends") {
edges {
node {
id
}
}
}
}
`)
```

## ⚖️&nbsp;Custom Scalars

Configuring your runtime to handle custom scalars is done under the `scalars` key in your config:
Expand Down
7 changes: 4 additions & 3 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "example-kit",
"private": true,
"version": "0.9.11",
"version": "0.10.0-alpha.13",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
Expand All @@ -12,8 +12,8 @@
"devDependencies": {
"@sveltejs/kit": "1.0.0-next.107",
"graphql": "15.5.0",
"houdini": "^0.9.11",
"houdini-preprocess": "^0.9.11",
"houdini": "^0.10.0-alpha.13",
"houdini-preprocess": "^0.10.0-alpha.13",
"svelte": "^3.38.2",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.2.0",
Expand All @@ -22,6 +22,7 @@
"type": "module",
"dependencies": {
"apollo-server": "^2.24.0",
"graphql-relay": "^0.8.0",
"subscriptions-transport-ws": "^0.9.18"
}
}
45 changes: 36 additions & 9 deletions example/schema/index.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const gql = require('graphql-tag')
const { PubSub, withFilter } = require('apollo-server')
const { GraphQLScalarType, Kind } = require('graphql')
const { connectionFromArray } = require('graphql-relay')

const pubsub = new PubSub()

Expand All @@ -20,7 +21,7 @@ module.exports.typeDefs = gql`
}
type Query {
items(completed: Boolean): [TodoItem!]!
items(first: Int, after: String, completed: Boolean): TodoItemConnection!
}
type Mutation {
Expand Down Expand Up @@ -57,25 +58,51 @@ module.exports.typeDefs = gql`
type ItemUpdate {
item: TodoItem!
}
`
id = 3
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type TodoItemConnection {
totalCount: Int!
pageInfo: PageInfo!
edges: [TodoItemEdge!]!
}
type TodoItemEdge {
cursor: String
node: TodoItem
}
`

// example data
let items = [
{ id: '1', text: 'Taste JavaScript', createdAt: new Date() },
{ id: '2', text: 'Buy a unicorn', createdAt: new Date() },
{ id: '3', text: 'Taste more JavaScript', createdAt: new Date() },
{ id: '4', text: 'Buy a another unicorn', createdAt: new Date() },
{ id: '5', text: 'Taste even more JavaScript', createdAt: new Date() },
{ id: '6', text: 'Buy a third unicorn', createdAt: new Date() },
]

id = items.length

module.exports.resolvers = {
Query: {
items: (_, { completed } = {}) => {
// if completed is undefined there is no filter
if (typeof completed === 'undefined') {
return items
}
items: (_, { completed, ...args } = {}) => {
const filtered = items.filter((item) =>
typeof completed === 'boolean'
? Boolean(item.completed) === Boolean(completed)
: true
)

const connection = connectionFromArray(filtered, args)
connection.totalCount = items.length

return items.filter((item) => Boolean(item.completed) === Boolean(completed))
return connection
},
},
Mutation: {
Expand Down
20 changes: 19 additions & 1 deletion example/schema/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Mutation {
}

type Query {
items(completed: Boolean): [TodoItem!]!
items(first: Int, after: String, completed: Boolean): TodoItemConnection!
}

type Subscription {
Expand All @@ -61,6 +61,24 @@ type UpdateItemOutput {
item: TodoItem
}

type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}

type TodoItemConnection {
totalCount: Int!
pageInfo: PageInfo!
edges: [TodoItemEdge!]!
}

type TodoItemEdge {
cursor: String
node: TodoItem
}

"""
The `Upload` scalar type represents a file upload.
"""
Expand Down
4 changes: 2 additions & 2 deletions example/src/lib/ItemEntry.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
item {
id
completed
...Filtered_Items_remove @when(argument: "completed", value: "false")
...Filtered_Items_remove @when(completed: false)
}
}
}
Expand All @@ -44,7 +44,7 @@
item {
id
completed
...Filtered_Items_remove @when(argument: "completed", value: "true")
...Filtered_Items_remove @when(completed: true)
}
}
}
Expand Down
Loading

0 comments on commit a52397c

Please sign in to comment.