Skip to content

Commit

Permalink
Merge pull request #246 from psteinroe/feat/cursor-pagination
Browse files Browse the repository at this point in the history
feat: cursor pagination
  • Loading branch information
psteinroe authored Jul 18, 2023
2 parents e0acb74 + f8b622c commit a6480be
Show file tree
Hide file tree
Showing 21 changed files with 798 additions and 116 deletions.
8 changes: 8 additions & 0 deletions .changeset/gentle-kiwis-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@supabase-cache-helpers/postgrest-fetcher": minor
"@supabase-cache-helpers/postgrest-filter": minor
"@supabase-cache-helpers/postgrest-swr": minor
---

- feat: add cursor pagination
- refactor: rename infinite queries to include the type (offset or cursor)
2 changes: 1 addition & 1 deletion docs/pages/postgrest/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If your package manager does not install peer dependencies automatically, you wi

## Quick Start

Import [`useQuery`](./queries#usequery) and define a simple query. The cache key is automatically created from the query. You can pass the SWR- and React Query-native options as a second argument. For pagination and infinite scroll queries, use [`usePaginationQuery`](./queries#usepaginationquery) and [`useInfiniteScrollQuery`](./queries#useinfinitescrollquery).
Import [`useQuery`](./queries#usequery) and define a simple query. The cache key is automatically created from the query. You can pass the SWR- and React Query-native options as a second argument. For pagination and infinite scroll queries, use [`useInfiniteOffsetPaginationQuery`](./queries#useinfiniteoffsetpaginationquery), [`useOffsetInfiniteScrollQuery`](./queries#useoffsetinfinitescrollquery) and [`useCursorInfiniteScrollQuery`](./queries#usecursorinfinitescrollquery).

<Tabs items={['SWR', 'React Query']}>
<Tab>
Expand Down
63 changes: 54 additions & 9 deletions docs/pages/postgrest/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Wrapper around the default data fetching hook that returns the query including t
</Tab>
</Tabs>

## `usePaginationQuery`
## `useInfiniteOffsetPaginationQuery`

Wrapper around the infinite hooks that transforms the data into pages and returns helper functions to paginate through them. The `range` filter is automatically applied based on the `pageSize` parameter. The respective configuration parameter can be passed as second argument.

Expand All @@ -99,7 +99,7 @@ The hook does not use a count query and therefore does not know how many pages t
<Tabs items={['SWR', 'React Query']}>
<Tab>
```tsx
import { usePaginationQuery } from '@supabase-cache-helpers/postgrest-swr';
import { useInfiniteOffsetPaginationQuery } from '@supabase-cache-helpers/postgrest-swr';
import { createClient } from '@supabase/supabase-js';
import { Database } from './types';

Expand All @@ -118,7 +118,7 @@ The hook does not use a count query and therefore does not know how many pages t
pageIndex,
isValidating,
error,
} = usePaginationQuery(
} = useInfiniteOffsetPaginationQuery(
client
.from('contact')
.select('id,username')
Expand All @@ -137,7 +137,7 @@ The hook does not use a count query and therefore does not know how many pages t
</Tab>
</Tabs>

## `useInfiniteScrollQuery`
## `useOffsetInfiniteScrollQuery`

Wrapper around the infinite hooks that transforms the data into a flat list and returns a `loadMore` function. The `range` filter is automatically applied based on the `pageSize` parameter. The `SWRConfigurationInfinite` can be passed as second argument.

Expand All @@ -148,7 +148,7 @@ The hook does not use a count query and therefore does not know how many items t
<Tabs items={['SWR', 'React Query']}>
<Tab>
```tsx
import { useInfiniteScrollQuery } from '@supabase-cache-helpers/postgrest-swr';
import { useOffsetInfiniteScrollQuery } from '@supabase-cache-helpers/postgrest-swr';
import { createClient } from '@supabase/supabase-js';
import { Database } from './types';

Expand All @@ -158,7 +158,7 @@ The hook does not use a count query and therefore does not know how many items t
);

function Page() {
const { data, loadMore, isValidating, error } = useInfiniteScrollQuery(
const { data, loadMore, isValidating, error } = useOffsetInfiniteScrollQuery(
client
.from('contact')
.select('id,username')
Expand All @@ -177,14 +177,59 @@ The hook does not use a count query and therefore does not know how many items t
</Tab>
</Tabs>

## `useInfiniteQuery`
## `useCursorInfiniteScrollQuery`

Similiar to `useOffsetInfiniteScrollQuery`, but instead of using the `offset` filter to paginate, it uses a cursor. You can find a longer rationale on why this is more performant than offset-based pagination [here](https://the-guild.dev/blog/graphql-cursor-pagination-with-postgresql#).

You define the column to be used as a cursor and the direction by passing an order filter. `loadMore()` is `undefined` if there is no more data to load.

The hook does not use a count query and therefore does not know how many items there are in total. You can pass `until`, to define until what value data should be loaded. If `until` is not defined, `loadMore` will always be truthy.

<Tabs items={['SWR', 'React Query']}>
<Tab>
```tsx
import { useCursorInfiniteScrollQuery } from '@supabase-cache-helpers/postgrest-swr';
import { createClient } from '@supabase/supabase-js';
import { Database } from './types';

const client = createClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);

function Page() {
const { data, loadMore, isValidating, error } = useCursorInfiniteScrollQuery(
client
.from('contact')
.select('id,username')
.eq('country', 'DE')
{
pageSize: 1,
until: `username-2`,
order: { column: 'username', ascending: true },
}
{ revalidateOnFocus: false }
);
return <div>...</div>;
}
```

</Tab>
<Tab>
```tsx
// not supported yet :(
```
</Tab>
</Tabs>

## `useOffsetInfiniteQuery`

Wrapper around the infinite hook that returns the query without any modification of the data.

<Tabs items={['SWR', 'React Query']}>
<Tab>
```tsx
import { useInfiniteQuery } from '@supabase-cache-helpers/postgrest-swr';
import { useOffsetInfiniteQuery } from '@supabase-cache-helpers/postgrest-swr';
import { createClient } from '@supabase/supabase-js';
import { Database } from './types';

Expand All @@ -194,7 +239,7 @@ Wrapper around the infinite hook that returns the query without any modification
);

function Page() {
const { data, size, setSize, isValidating, error, mutate } = useInfiniteQuery(
const { data, size, setSize, isValidating, error, mutate } = useOffsetInfiniteQuery(
client
.from('contact')
.select('id,username')
Expand Down
4 changes: 2 additions & 2 deletions examples/swr/pages/use-infinite-scroll-query.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from "react"
import Head from "next/head"
import { useInfiniteScrollQuery } from "@supabase-cache-helpers/postgrest-swr"
import { useOffsetInfiniteScrollQuery } from "@supabase-cache-helpers/postgrest-swr"
import { useSupabaseClient } from "@supabase/auth-helpers-react"
import { ArrowDown, Loader2 } from "lucide-react"
import { z } from "zod"
Expand All @@ -27,7 +27,7 @@ export default function UseInfiniteScrollQueryPage() {
isValidating,
isLoading,
loadMore,
} = useInfiniteScrollQuery(
} = useOffsetInfiniteScrollQuery(
supabase
.from("contact")
.select("id,username,continent")
Expand Down
29 changes: 15 additions & 14 deletions examples/swr/pages/use-pagination-query.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from "react"
import Head from "next/head"
import { usePaginationQuery } from "@supabase-cache-helpers/postgrest-swr"
import { useInfiniteOffsetPaginationQuery } from "@supabase-cache-helpers/postgrest-swr"
import { useSupabaseClient } from "@supabase/auth-helpers-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { z } from "zod"
Expand All @@ -22,19 +22,20 @@ import { Button } from "@/components/ui/button"

export default function UsePaginationQueryPage() {
const supabase = useSupabaseClient<Database>()
const { currentPage, previousPage, nextPage } = usePaginationQuery(
supabase
.from("contact")
.select("id,username,continent")
.order("username")
.returns<
(Pick<
Database["public"]["Tables"]["contact"]["Row"],
"id" | "username"
> & { continent: z.infer<typeof continentEnumSchema> })[]
>(),
{ revalidateOnFocus: false, pageSize: 5 }
)
const { currentPage, previousPage, nextPage } =
useInfiniteOffsetPaginationQuery(
supabase
.from("contact")
.select("id,username,continent")
.order("username")
.returns<
(Pick<
Database["public"]["Tables"]["contact"]["Row"],
"id" | "username"
> & { continent: z.infer<typeof continentEnumSchema> })[]
>(),
{ revalidateOnFocus: false, pageSize: 5 }
)

const [upsertContact, setUpsertContact] = useState<
UpsertContactFormData | boolean
Expand Down
123 changes: 123 additions & 0 deletions packages/postgrest-fetcher/__tests__/cursor-pagination-fetcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { Database } from './database.types';
import './utils';

import { createCursorPaginationFetcher } from '../src';

const TEST_PREFIX = 'postgrest-fetcher-cursor-pagination-fetcher-';

describe('cursor-pagination-fetcher', () => {
let client: SupabaseClient<Database>;
let testRunPrefix: string;
let contacts: Database['public']['Tables']['contact']['Row'][];

beforeAll(async () => {
testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`;
client = createClient(
process.env.SUPABASE_URL as string,
process.env.SUPABASE_ANON_KEY as string
);
await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`);

const { data } = await client
.from('contact')
.insert([
{ username: `${testRunPrefix}-username-1` },
{ username: `${testRunPrefix}-username-2` },
{ username: `${testRunPrefix}-username-3` },
{ username: `${testRunPrefix}-username-4` },
])
.select('*')
.throwOnError();
contacts = data ?? [];
expect(contacts).toHaveLength(4);
});

describe('createCursorPaginationFetcher', () => {
it('should return null if query is undefined', () => {
expect(
createCursorPaginationFetcher(
null,
{
order: { column: 'username', ascending: true, nullsFirst: false },
},
(key) => ({
cursor: `${testRunPrefix}-username-2`,
})
)
).toEqual(null);
});

it('should work with no cursor', async () => {
const fetcher = createCursorPaginationFetcher(
client
.from('contact')
.select('username')
.ilike('username', `${testRunPrefix}%`)
.order('username', { ascending: true, nullsFirst: false })
.limit(2),
{
order: { column: 'username', ascending: true, nullsFirst: false },
},
() => ({
cursor: undefined,
})
);
expect(fetcher).toBeDefined();
const data = await fetcher!('');
expect(data).toHaveLength(2);
expect(data).toEqual([
{ username: `${testRunPrefix}-username-1` },
{ username: `${testRunPrefix}-username-2` },
]);
});

it('should apply cursor from key', async () => {
const fetcher = createCursorPaginationFetcher(
client
.from('contact')
.select('username')
.ilike('username', `${testRunPrefix}%`)
.limit(2)
.order('username', { ascending: true, nullsFirst: false }),
{
order: { column: 'username', ascending: true, nullsFirst: false },
},
(key) => ({
cursor: `${testRunPrefix}-username-2`,
})
);
expect(fetcher).toBeDefined();
const data = await fetcher!('');
expect(data).toHaveLength(2);
expect(data).toEqual([
{ username: `${testRunPrefix}-username-3` },
{ username: `${testRunPrefix}-username-4` },
]);
});

it('should work descending', async () => {
const fetcher = createCursorPaginationFetcher(
client
.from('contact')
.select('username')
.ilike('username', `${testRunPrefix}%`)
.limit(2)
.order('username', { ascending: true, nullsFirst: false }),
{
order: { column: 'username', ascending: false, nullsFirst: false },
},
(key) => ({
cursor: `${testRunPrefix}-username-3`,
})
);
expect(fetcher).toBeDefined();
const data = await fetcher!('');
expect(data).toHaveLength(2);
expect(data).toEqual([
{ username: `${testRunPrefix}-username-1` },
{ username: `${testRunPrefix}-username-2` },
]);
});
});
});
Loading

3 comments on commit a6480be

@vercel
Copy link

@vercel vercel bot commented on a6480be Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

supabase-cache-helpers-react-query – ./examples/react-query

supabase-cache-helpers-react-query-git-main-psteinroe.vercel.app
supabase-cache-helpers-react-query-psteinroe.vercel.app
supabase-cache-helpers-react-query.vercel.app

@vercel
Copy link

@vercel vercel bot commented on a6480be Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on a6480be Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

supabase-cache-helpers-swr-demo – ./examples/swr

supabase-cache-helpers-swr-demo-git-main-psteinroe.vercel.app
supabase-cache-helpers-swr-demo-psteinroe.vercel.app
supabase-cache-helpers-swr.vercel.app

Please sign in to comment.