diff --git a/.changeset/tame-berries-ring.md b/.changeset/tame-berries-ring.md new file mode 100644 index 0000000..1ab8ab9 --- /dev/null +++ b/.changeset/tame-berries-ring.md @@ -0,0 +1,5 @@ +--- +'svelte-query-pocketbase': patch +--- + +chore: prepare for first release diff --git a/LICENSE.md b/LICENSE.md index e3b8465..3540832 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,5 @@ The MIT License (MIT) -Copyright (c) 2022 - present, Gani Georgiev +Copyright (c) 2023 - present, Akaanksh Raj Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, diff --git a/README.md b/README.md index 3f6f992..b3e8f0a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,693 @@ # svelte-query-pocketbase -TanStack Query wrappers around Pocketbase Realtime for Svelte +TanStack Query wrappers around Pocketbase Realtime for Svelte. ## Installation ``` -npm i -D svelte-query-pocketbase@beta +npm i -D svelte-query-pocketbase +``` + +## Record Query + +Creates a TanStack Query that updates a Pocketbase record in realtime. View the JSDoc for the relevant functions for more documentation on their available options. + +### Simple Example + +```svelte + + +{#if $someRecord.data} +

Fetched record:

+
{JSON.stringify($someRecord.data, null, 2)}
+{:else if $someRecord.error} + {#if $someRecord.error.status === 404} +

The record couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### With Query Params + +```svelte + + +{#if $someRecord.data} +

Fetched record, with some_field expanded:

+
{JSON.stringify($someRecord.data, null, 2)}
+{:else if $someRecord.error} + {#if $someRecord.error.status === 404} +

The record couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### Using SSR + +Read [TanStack Query's docs on this](https://tanstack.com/query/v4/docs/svelte/ssr) first. The examples below are modified versions of the examples on that page. + +#### Using `initialData` + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { createRecordQueryInitialData } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async () => { + const someIdInitialData = await createRecordQueryInitialData( + pocketbase.collection(Collections.SomeCollection), + 'some_id' + ); + return { someIdInitialData }; +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +#### Using `prefetchQuery` + +**src/routes/+layout.ts** + +_Same as TanStack Query's docs_ + +**src/routes/+layout.svelte** + +_Same as TanStack Query's docs_ + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { createRecordQueryPrefetch } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async ({ parent }) => { + const { queryClient } = await parent(); + + // As long as the same collection, id, and queryParams are supplied to + // `createRecordQueryPrefetch` and `createRecordQuery`, the library will + // generate the same `queryKey`s for both functions, and you need not specify one + await queryClient.prefetchQuery( + createRecordQueryPrefetch( + pocketbase.collection(Collections.SomeCollection), + 'some_id' + ) + ); +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +## Collection Query + +Creates a TanStack Query that updates an array of Pocketbase records in realtime. View the JSDoc for the relevant functions for more documentation on their available options. + +### Simple Example + +```svelte + + +{#if $someCollection.data} +

Fetched collection:

+
{JSON.stringify($someCollection.data, null, 2)}
+{:else if $someCollection.error} + {#if $someCollection.error.status === 404} +

The collection couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### With Query Params + +```svelte + + +{#if $someCollection.data} +

+ Fetched collection, with some_field expanded, sorted by date created (descending), and filtered + by date created after 2022-01-01 00:00:00: +

+
{JSON.stringify($someCollection.data, null, 2)}
+{:else if $someCollection.error} + {#if $someCollection.error.status === 404} +

The collection couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### Using SSR + +Read [TanStack Query's docs on this](https://tanstack.com/query/v4/docs/svelte/ssr) first. The examples below are modified versions of the examples on that page. + +#### Using `initialData` + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { createCollectionQueryInitialData } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async () => { + const someCollectionInitialData = await createCollectionQueryInitialData( + pocketbase.collection(Collections.SomeCollection) + ); + return { someCollectionInitialData }; +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +#### Using `prefetchQuery` + +**src/routes/+layout.ts** + +_Same as TanStack Query's docs_ + +**src/routes/+layout.svelte** + +_Same as TanStack Query's docs_ + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { createCollectionQueryPrefetch } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async ({ parent }) => { + const { queryClient } = await parent(); + + // As long as the same collection, id, and queryParams are supplied to + // `createCollectionQueryPrefetch` and `createCollectionQuery`, the library will + // generate the same `queryKey`s for both functions, and you need not specify one + await queryClient.prefetchQuery( + createCollectionQueryPrefetch( + pocketbase.collection(Collections.SomeCollection) + ) + ); +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +## Infinite Collection Query + +Creates a TanStack Infinite Query that updates paginated Pocketbase records in realtime. View the JSDoc for the relevant functions for more documentation on their available options. + +### Simple Example + +```svelte + + +{#if $someInfiniteCollection.data} +

Fetched infinite collection:

+
{JSON.stringify($someInfiniteCollection.data, null, 2)}
+ {#if $someInfiniteCollection.hasNextPage} + + {/if} +{:else if $someInfiniteCollection.error} + {#if $someInfiniteCollection.error.status === 404} +

The collection couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### With Query Params + +```svelte + + +{#if $someInfiniteCollection.data} +

+ Fetched infinite collection, with some_field expanded, sorted by date created (descending), and + filtered by date created after 2022-01-01 00:00:00: +

+
{JSON.stringify($someInfiniteCollection.data, null, 2)}
+ {#if $someInfiniteCollection.hasNextPage} + + {/if} +{:else if $someInfiniteCollection.error} + {#if $someInfiniteCollection.error.status === 404} +

The collection couldn't be found in the database.

+ {:else} +

Something went wrong.

+ + {/if} +{:else} +

Loading...

+{/if} +``` + +### Using SSR + +Read [TanStack Query's docs on this](https://tanstack.com/query/v4/docs/svelte/ssr) first. The examples below are modified versions of the examples on that page. + +#### Using `initialData` + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { infiniteCollectionQueryInitialData } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async () => { + const someInfiniteCollectionInitialData = + await infiniteCollectionQueryInitialData( + pocketbase.collection(Collections.SomeCollection) + ); + return { someInfiniteCollectionInitialData }; +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +#### Using `prefetchQuery` + +**src/routes/+layout.ts** + +_Same as TanStack Query's docs_ + +**src/routes/+layout.svelte** + +_Same as TanStack Query's docs_ + +**src/routes/+page.ts** + +```ts +import type { PageLoad } from './$types'; + +import Pocketbase from 'pocketbase'; +import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +// Types generated from https://github.com/patmood/pocketbase-typegen +import { Collections, type SomeCollectionResponse } from '$lib/collections'; + +import { infiniteCollectionQueryPrefetch } from 'svelte-query-pocketbase'; + +export const load: PageLoad = async ({ parent }) => { + const { queryClient } = await parent(); + + // As long as the same collection, id, and queryParams are supplied to + // `infiniteCollectionQueryPrefetch` and `createCollectionQuery`, the library will + // generate the same `queryKey`s for both functions, and you need not specify one + await queryClient.prefetchQuery( + infiniteCollectionQueryPrefetch( + pocketbase.collection(Collections.SomeCollection) + ) + ); +}; +``` + +**src/routes/+page.svelte** + +```svelte + +``` + +## User Store + +Svelte store wrapper around the authenticated Pocketbase user that updates in realtime. + +### Using Default Auth Store + +```svelte + + +{#if $user.isLoggedIn} +

Welcome, {$user.name}:

+
{JSON.stringify($user, null, 2)}
+{:else} + You are not logged in. +{/if} +``` + +### Using Local Auth Store + +```svelte + + +{#if $user.isLoggedIn} +

Welcome, {$user.name}:

+
{JSON.stringify($user, null, 2)}
+{:else} + You are not logged in. +{/if} ``` diff --git a/package.json b/package.json index 4d27276..9867b3c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@commitlint/cli": "17.4.2", "@commitlint/config-conventional": "17.4.2", "@commitlint/prompt-cli": "17.4.2", - "@square/svelte-store": "1.0.13", "@sveltejs/adapter-node": "1.1.4", "@sveltejs/kit": "1.3.2", "@types/lodash": "4.14.191", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9e794..9e634e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ specifiers: '@commitlint/cli': 17.4.2 '@commitlint/config-conventional': 17.4.2 '@commitlint/prompt-cli': 17.4.2 - '@square/svelte-store': 1.0.13 '@sveltejs/adapter-node': 1.1.4 '@sveltejs/kit': 1.3.2 '@tanstack/svelte-query': ^4.22.2 @@ -44,7 +43,6 @@ devDependencies: '@commitlint/cli': 17.4.2 '@commitlint/config-conventional': 17.4.2 '@commitlint/prompt-cli': 17.4.2 - '@square/svelte-store': 1.0.13 '@sveltejs/adapter-node': 1.1.4_@sveltejs+kit@1.3.2 '@sveltejs/kit': 1.3.2_svelte@3.55.1+vite@4.0.4 '@types/lodash': 4.14.191 @@ -888,13 +886,6 @@ packages: rollup: 3.10.0 dev: true - /@square/svelte-store/1.0.13: - resolution: {integrity: sha512-UAspWhgQO6rPAU7WTImPPyfRmfV4GIvD1gIQhlZRNo3cYdbgfNBkWkxcZkZwg2vmhhQ6x42xt8m9FOrazQXBmQ==} - dependencies: - cookie-storage: 6.1.0 - svelte: 3.55.1 - dev: true - /@sveltejs/adapter-node/1.1.4_@sveltejs+kit@1.3.2: resolution: {integrity: sha512-3iEBqi1fXLXP9YIbVuz2LXajoebRJCmAFEQbN40DlxAnA7G+InxUgnqFun3q9gBMz2Qvd99K51g/HxWetXRe8Q==} peerDependencies: @@ -1704,10 +1695,6 @@ packages: through2: 4.0.2 dev: true - /cookie-storage/6.1.0: - resolution: {integrity: sha512-HeVqbVy8BjXhAAuFtL6MTG+witHoLbxfky2jgVh9FmxmyL6IKa9gSSyPNjevXCCCxPu6Tzd9J8+eXTRQzYU/cg==} - dev: true - /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} diff --git a/src/lib/internal/index.ts b/src/lib/internal/index.ts index 5e3fd42..32211f0 100644 --- a/src/lib/internal/index.ts +++ b/src/lib/internal/index.ts @@ -1,6 +1,14 @@ import type Client from 'pocketbase'; import type { Record } from 'pocketbase'; +/** + * Meant for internal use, simply returns the expanded record if there is an expand query param. + * + * @param collection Collection to call `getOne` on. + * @param record The record to be expanded. + * @param [expand] The expand query param. + * @returns The expanded record. + */ export const realtimeStoreExpand = async >( collection: ReturnType, record: T, diff --git a/src/lib/queries/collection.ts b/src/lib/queries/collection.ts index c349ce0..dd2fe91 100644 --- a/src/lib/queries/collection.ts +++ b/src/lib/queries/collection.ts @@ -19,7 +19,7 @@ import type { import { collectionKeys } from '../query-key-factory'; import { realtimeStoreExpand } from '../internal'; -import type { CollectionStoreOptions, QueryPrefetchOptions } from '../types'; +import type { CollectionStoreOptions, CollectionQueryPrefetchOptions } from '../types'; setAutoFreeze(false); @@ -96,6 +96,13 @@ const collectionStoreCallback = async < } }; +/** + * Meant for SSR use, simply returns all the records from a collection. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-initialdata) and this project's README.md for some examples. + * + * @param collection The collection from which to get all the records. + * @param [options.queryParams] The query params that will be passed on to `getFullList`. + * @returns The initial data required for a collection query, i.e. an array of Pocketbase records. + */ export const createCollectionQueryInitialData = async < T extends Pick = Pick >( @@ -103,6 +110,16 @@ export const createCollectionQueryInitialData = async < { queryParams = undefined }: { queryParams?: RecordListQueryParams } ): Promise> => [...(await collection.getFullList(undefined, queryParams))]; +/** + * Meant for SSR use, allows for prefetching queries on the server so that data is already available in the cache, and no initial fetch occurs client-side. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-prefetchquery) and this project's README.md for some examples. + * + * @param collection The collection from which to get all the records. + * @param [options.queryParams] The query params are simply passed on to `getFullList` for the initial data fetch, and `getOne` for realtime updates. If the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.queryKey] Provides the query key option for TanStack Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options] The rest of the options are passed to the `prefetchQuery` function from TanStack Query, this library has no defaults for them. + * @returns The fully-configured options object that is ready to be passed on to the `prefetchQuery` function from TanStack Query. + */ export const createCollectionQueryPrefetch = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -116,7 +133,7 @@ export const createCollectionQueryPrefetch = < ...(queryParams && queryParams) }) as unknown as TQueryKey, ...options - }: QueryPrefetchOptions, ClientResponseError, Array, TQueryKey> = {} + }: CollectionQueryPrefetchOptions, ClientResponseError, Array, TQueryKey> = {} ): FetchQueryOptions, ClientResponseError, Array, TQueryKey> => ({ staleTime, queryKey, @@ -124,6 +141,29 @@ export const createCollectionQueryPrefetch = < queryFn: async () => await createCollectionQueryInitialData(collection, { queryParams }) }); +/** + * Creates a TanStack Query that updates an array of Pocketbase records in realtime. + * + * Notes: + * - When running server-side, the realtime subscription will not be created. + * - When a realtime update is received, after the action is handled, the `filterFunction` runs first, then `sortFunction` runs. + * - If a `create` action is received via the realtime subscription, the new record is added to the end of the query's data array before the `filterFunction` and `sortFunction` run. + * + * @param collection The collection from which to get all the records. + * @param [options.queryParams] The query params are simply passed on to `getFullList` for the initial data fetch, and `getOne` for realtime updates. If the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.sortFunction] `compareFn` from `Array.prototype.sort` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `sort` in `queryParams`. + * @param [options.filterFunction] `predicate` from `Array.prototype.filter` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `filter` in `queryParams`. + * @param [options.filterFunctionThisArg] `thisArg` from `Array.prototype.filter` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `filter` in `queryParams`. + * @param [options.disableRealtime] Provides an option to disable realtime updates to the array of Pocketbase records. By default, `false` since we want the array of Pocketbase records to be updated in realtime. If set to `true`, a realtime subscription to the Pocketbase server is never sent. Don't forget to set `options.staleTime` to a more appropriate value than `Infinity` you disable realtime updates. + * @param [options.invalidateQueryOnRealtimeError] Provides an option to invalidate the query if a realtime error occurs. By default, `true` since if a realtime error occurs, the query's data would be stale. + * @param [options.onRealtimeUpdate] This function is called with the realtime action every time an realtime action is received. + * @param [options.queryKey] Provides the query key option for TanStack Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options.refetchOnReconnect] Provides the refetch on reconnection option for TanStack Query. By default, `'always'` since the query we wouldn't be receiving realtime updates if connection was lost. + * @param [options.enable] Provides the enabled option for TanStack Query. By default, `true` since we want the query to be enabled. If set to `false`, this also disables realtime updates to the Pocketbase record. + * @param [options] The rest of the options are passed to the `createQuery` function from TanStack Query, this library has no defaults for them. + * @returns The same object as TanStack Query's `createQuery`, with an array of Pocketbase records that updates in realtime. + */ export const createCollectionQuery = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -140,6 +180,7 @@ export const createCollectionQuery = < enabled = true, disableRealtime = false, invalidateQueryOnRealtimeError = true, + debug = false, ...options }: CollectionStoreOptions> = {} ): CreateQueryResult => { @@ -170,17 +211,19 @@ export const createCollectionQuery = < options.filterFunctionThisArg ) .then(() => { - console.log( - `(C) ${JSON.stringify(queryKey)}: updating with realtime action:`, - data.action, - data.record.id - ); + debug && + console.log( + `(C) ${JSON.stringify(queryKey)}: updating with realtime action:`, + data.action, + data.record.id + ); }) .catch((e) => { - console.log( - `(C) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, - e - ); + debug && + console.log( + `(C) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -188,12 +231,13 @@ export const createCollectionQuery = < options.onRealtimeUpdate?.(data); }) .catch((e) => { - console.log( - `(C) [${JSON.stringify( - queryKey - )}]: invalidating query due to realtime subscription error:`, - e - ); + debug && + console.log( + `(C) [${JSON.stringify( + queryKey + )}]: invalidating query due to realtime subscription error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -202,10 +246,10 @@ export const createCollectionQuery = < return { subscribe: (...args) => { - console.log(`(C) ${JSON.stringify(queryKey)}: subscribing to changes...`); + debug && console.log(`(C) ${JSON.stringify(queryKey)}: subscribing to changes...`); let unsubscriber = store.subscribe(...args); return () => { - console.log(`(C) ${JSON.stringify(queryKey)}: unsubscribing from store.`); + debug && console.log(`(C) ${JSON.stringify(queryKey)}: unsubscribing from store.`); (async () => { await ( await unsubscribePromise @@ -216,11 +260,12 @@ export const createCollectionQuery = < ) || Object.keys(queryParams ?? {}).length > 0 ) { - console.log( - `(C) ${JSON.stringify( - queryKey - )}: no realtime listeners or query has queryParams, marking query as stale.` - ); + debug && + console.log( + `(C) ${JSON.stringify( + queryKey + )}: no realtime listeners or query has queryParams, marking query as stale.` + ); queryClient.invalidateQueries({ queryKey, exact: true }); } })(); diff --git a/src/lib/queries/infinite-collection.ts b/src/lib/queries/infinite-collection.ts index f801e0b..a8ee517 100644 --- a/src/lib/queries/infinite-collection.ts +++ b/src/lib/queries/infinite-collection.ts @@ -225,6 +225,15 @@ const infiniteCollectionStoreCallback = async < } }; +/** + * Meant for SSR use, simply returns paginated records from a collection. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-initialdata) and this project's README.md for some examples. + * + * @param collection The collection from which to get paginated records. + * @param [options.page] The page that will be passed on to `getList`. By default, `1`. + * @param [options.perPage] The per page that will be passed on to `getList`. By default, `20`. + * @param [options.queryParams] The query params that will be passed on to `getList`. + * @returns The initial data required for a collection query, i.e. an array of Pocketbase records. + */ export const infiniteCollectionQueryInitialData = async < T extends Pick = Pick >( @@ -236,6 +245,19 @@ export const infiniteCollectionQueryInitialData = async < }: { page?: number; perPage?: number; queryParams?: RecordListQueryParams } = {} ): Promise> => ({ ...(await collection.getList(page, perPage, queryParams)) }); +/** + * Meant for SSR use, allows for prefetching queries on the server so that data is already available in the cache, and no initial fetch occurs client-side. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-prefetchquery) and this project's README.md for some examples. + * + * @param collection The collection from which to get paginated records. + * @param [options.page] The page that will be passed on to `getList`. By default, `1`. + * @param [options.perPage] The per page that will be passed on to `getList`. By default, `20`. + * @param [options.queryParams] The query params are simply passed on to `getList` for the initial data fetch, and `getOne` for realtime updates. If the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.queryKey] Provides the query key option for TanStack Infinite Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Infinite Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options] The rest of the options are passed to the `prefetchQuery` function from TanStack Infinite Query, this library has no defaults for them. + * @returns The fully-configured options object that is ready to be passed on to the `prefetchQuery` function from TanStack Query. + */ + export const infiniteCollectionQueryPrefetch = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -262,6 +284,32 @@ export const infiniteCollectionQueryPrefetch = < await infiniteCollectionQueryInitialData(collection, { page, perPage, queryParams }) }); +/** + * Creates a TanStack Infinite Query that updates paginated Pocketbase records in realtime. + * + * Notes: + * - When running server-side, the realtime subscription will not be created. + * - When a realtime update is received, after the action is handled, the `filterFunction` runs first, then `sortFunction` runs, then the records are chunked into pages and the relevant pages are marked as stale, if needed. + * + * @param collection The collection from which to get paginated records. + * @param [options.page] The page that will be passed on to `getList`. By default, `1`. + * @param [options.perPage] The per page that will be passed on to `getList`. By default, `20`. + * @param [options.queryParams] The query params are simply passed on to `getFullList` for the initial data fetch, and `getOne` for realtime updates. If the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.keepCurrentPageOnly] Only keeps data from the current page of the infinite query, and discards the rest of the data when a page is changed. + * @param [options.sortFunction] `compareFn` from `Array.prototype.sort` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `sort` in `queryParams`. + * @param [options.filterFunction] `predicate` from `Array.prototype.filter` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `filter` in `queryParams`. + * @param [options.filterFunctionThisArg] `thisArg` from `Array.prototype.filter` that runs when an action is received via the realtime subscription. This is used since Pocketbase realtime subscriptions does not support `filter` in `queryParams`. + * @param [options.disableRealtime] Provides an option to disable realtime updates to the array of Pocketbase records. By default, `false` since we want the array of Pocketbase records to be updated in realtime. If set to `true`, a realtime subscription to the Pocketbase server is never sent. Don't forget to set `options.staleTime` to a more appropriate value than `Infinity` you disable realtime updates. + * @param [options.invalidateQueryOnRealtimeError] Provides an option to invalidate the query if a realtime error occurs. By default, `true` since if a realtime error occurs, the query's data would be stale. + * @param [options.onRealtimeUpdate] This function is called with the realtime action every time an realtime action is received. + * @param [options.queryKey] Provides the query key option for TanStack Infinite Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Infinite Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options.refetchOnReconnect] Provides the refetch on reconnection option for TanStack Infinite Query. By default, `'always'` since the query we wouldn't be receiving realtime updates if connection was lost. + * @param [options.enable] Provides the enabled option for TanStack Infinite Query. By default, `true` since we want the query to be enabled. If set to `false`, this also disables realtime updates to the Pocketbase record. + * @param [options] The rest of the options are passed to the `createQuery` function from TanStack Infinite Query, this library has no defaults for them. + * @returns The same object as TanStack Query's `createInfiniteQuery`, with paginated Pocketbase records that update in realtime. + */ + export const createInfiniteCollectionQuery = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -283,6 +331,7 @@ export const createInfiniteCollectionQuery = < disableRealtime = false, invalidateQueryOnRealtimeError = true, keepCurrentPageOnly = false, + debug = false, ...options }: InfiniteCollectionStoreOptions< ListResult, @@ -359,17 +408,19 @@ export const createInfiniteCollectionQuery = < options.filterFunctionThisArg ) .then(() => { - console.log( - `(IC) ${JSON.stringify(queryKey)}: updating with realtime action:`, - data.action, - data.record.id - ); + debug && + console.log( + `(IC) ${JSON.stringify(queryKey)}: updating with realtime action:`, + data.action, + data.record.id + ); }) .catch((e) => { - console.log( - `(IC) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, - e - ); + debug && + console.log( + `(IC) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -377,12 +428,13 @@ export const createInfiniteCollectionQuery = < options.onRealtimeUpdate?.(data); }) .catch((e) => { - console.log( - `(IC) [${JSON.stringify( - queryKey - )}]: invalidating query due to realtime subscription error:`, - e - ); + debug && + console.log( + `(IC) [${JSON.stringify( + queryKey + )}]: invalidating query due to realtime subscription error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -391,15 +443,15 @@ export const createInfiniteCollectionQuery = < return { subscribe: (...args) => { - console.log(`(IC) ${JSON.stringify(queryKey)}: subscribing to changes...`); + debug && console.log(`(IC) ${JSON.stringify(queryKey)}: subscribing to changes...`); let unsubscriber = store.subscribe(...args); return () => { - console.log(`(IC) ${JSON.stringify(queryKey)}: unsubscribing from store.`); + debug && console.log(`(IC) ${JSON.stringify(queryKey)}: unsubscribing from store.`); (async () => { await ( await unsubscribePromise )(); - console.log(`(IC) ${JSON.stringify(queryKey)}: marking query as stale.`); + debug && console.log(`(IC) ${JSON.stringify(queryKey)}: marking query as stale.`); queryClient.invalidateQueries({ queryKey, exact: true }); })(); return unsubscriber(); diff --git a/src/lib/queries/record.ts b/src/lib/queries/record.ts index 663a064..0880740 100644 --- a/src/lib/queries/record.ts +++ b/src/lib/queries/record.ts @@ -58,6 +58,14 @@ const createRecordQueryCallback = async < } }; +/** + * Meant for SSR use, simply returns the record from a collection. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-initialdata) and this project's README.md for some examples. + * + * @param collection The collection from which to get the record. + * @param id The `id` of the record to get from the collection. + * @param [options.queryParams] The query params that will be passed on to `getOne`. + * @returns The initial data required for a record query, i.e. the Pocketbase record. + */ export const createRecordQueryInitialData = < T extends Pick = Pick >( @@ -66,6 +74,17 @@ export const createRecordQueryInitialData = < { queryParams = undefined }: { queryParams?: RecordQueryParams } ): Promise => collection.getOne(id, queryParams); +/** + * Meant for SSR use, allows for prefetching queries on the server so that data is already available in the cache, and no initial fetch occurs client-side. See [TanStack's documentation](https://tanstack.com/query/v4/docs/svelte/ssr#using-prefetchquery) and this project's README.md for some examples. + * + * @param collection The collection from which to get the record. + * @param id The `id` of the record to get from the collection. + * @param [options.queryParams] The query params are simply passed on to `getOne` and if the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.queryKey] Provides the query key option for TanStack Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options] The rest of the options are passed to the `prefetchQuery` function from TanStack Query, this library has no defaults for them. + * @returns The fully-configured options object that is ready to be passed on to the `prefetchQuery` function from TanStack Query. + */ export const createRecordQueryPrefetch = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -89,6 +108,26 @@ export const createRecordQueryPrefetch = < queryFn: async () => await createRecordQueryInitialData(collection, id, { queryParams }) }); +/** + * Creates a TanStack Query that updates a Pocketbase record in realtime. + * + * Notes: + * - If a `delete` action is received via the realtime subscription, this query's data value changes to `null`. + * - When running server-side, the realtime subscription will not be created. + * + * @param collection The collection from which to get the record. + * @param id The `id` of the record to get from the collection. + * @param [options.queryParams] The query params are simply passed on to `getOne` and if the `expand` key is provided, the record is expanded every time a realtime update is received. + * @param [options.disableRealtime] Provides an option to disable realtime updates to the Pocketbase record. By default, `false` since we want the Pocketbase record to be updated in realtime. If set to `true`, a realtime subscription to the Pocketbase server is never sent. Don't forget to set `options.staleTime` to a more appropriate value than `Infinity` you disable realtime updates. + * @param [options.invalidateQueryOnRealtimeError] Provides an option to invalidate the query if a realtime error occurs. By default, `true` since if a realtime error occurs, the query's data would be stale. + * @param [options.onRealtimeUpdate] This function is called with the realtime action every time an realtime action is received. + * @param [options.queryKey] Provides the query key option for TanStack Query. By default, uses the `collectionKeys` function from this package. + * @param [options.staleTime] Provides the stale time option for TanStack Query. By default, `Infinity` since the query receives realtime updates. Note that the package will take care of automatically marking the query as stale when the last subscriber unsubscribes from this query. + * @param [options.refetchOnReconnect] Provides the refetch on reconnection option for TanStack Query. By default, `'always'` since the query we wouldn't be receiving realtime updates if connection was lost. + * @param [options.enable] Provides the enabled option for TanStack Query. By default, `true` since we want the query to be enabled. If set to `false`, this also disables realtime updates to the Pocketbase record. + * @param [options] The rest of the options are passed to the `createQuery` function from TanStack Query, this library has no defaults for them. + * @returns The same object as TanStack Query's `createQuery`, with a Pocketbase record that updates in realtime. + */ export const createRecordQuery = < T extends Pick = Pick, TQueryKey extends QueryKey = QueryKey @@ -107,6 +146,7 @@ export const createRecordQuery = < enabled = true, disableRealtime = false, invalidateQueryOnRealtimeError = true, + debug = false, ...options }: RecordStoreOptions< T | null, @@ -134,17 +174,19 @@ export const createRecordQuery = < .subscribe(id, (data) => { createRecordQueryCallback(queryClient, queryKey, data, collection, queryParams) .then(() => { - console.log( - `(R) ${JSON.stringify(queryKey)}: updating with realtime action:`, - data.action, - data.record.id - ); + debug && + console.log( + `(R) ${JSON.stringify(queryKey)}: updating with realtime action:`, + data.action, + data.record.id + ); }) .catch((e) => { - console.log( - `(R) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, - e - ); + debug && + console.log( + `(R) ${JSON.stringify(queryKey)}: invalidating query due to callback error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -152,12 +194,13 @@ export const createRecordQuery = < options.onRealtimeUpdate?.(data); }) .catch((e) => { - console.log( - `(R) [${JSON.stringify( - queryKey - )}]: invalidating query due to realtime subscription error:`, - e - ); + debug && + console.log( + `(R) [${JSON.stringify( + queryKey + )}]: invalidating query due to realtime subscription error:`, + e + ); if (invalidateQueryOnRealtimeError) { queryClient.invalidateQueries({ queryKey, exact: true }); } @@ -166,10 +209,10 @@ export const createRecordQuery = < return { subscribe: (...args) => { - console.log(`(R) ${JSON.stringify(queryKey)}: subscribing to changes...`); + debug && console.log(`(R) ${JSON.stringify(queryKey)}: subscribing to changes...`); let unsubscriber = store.subscribe(...args); return () => { - console.log(`(R) ${JSON.stringify(queryKey)}: unsubscribing from store.`); + debug && console.log(`(R) ${JSON.stringify(queryKey)}: unsubscribing from store.`); (async () => { await ( await unsubscribePromise @@ -180,14 +223,12 @@ export const createRecordQuery = < ) || Object.keys(queryParams ?? {}).length > 0 ) { - console.log( - `(R) ${JSON.stringify( - queryKey - )}: no realtime listeners or query has queryParams, marking query as stale.` - ); - // todo: correctly derive queryKey to mark as invalid - // todo: ensure that if a $store was passed into filterFunction, - // only the value when query was created with is used even after the $store's value changes + debug && + console.log( + `(R) ${JSON.stringify( + queryKey + )}: no realtime listeners or query has queryParams, marking query as stale.` + ); queryClient.invalidateQueries({ queryKey, exact: true }); } })(); diff --git a/src/lib/queries/user.ts b/src/lib/queries/user.ts index 5994e9f..9b935ac 100644 --- a/src/lib/queries/user.ts +++ b/src/lib/queries/user.ts @@ -1,11 +1,11 @@ import type Client from 'pocketbase'; import type { BaseAuthStore, Record } from 'pocketbase'; -import { type Loadable, readable } from '@square/svelte-store'; +import { readable, type Readable } from 'svelte/store'; import { type KnownUser, type UnknownUser, isRecord } from '../types'; /** - * Svelte store wrapper around the authenticated user that updates in realtime. + * Svelte store wrapper around the authenticated Pocketbase user that updates in realtime. * * NOTE: This store returns an `UnknownUser` if the authenticated user is `Admin`. * @@ -19,7 +19,7 @@ export const userStore = < pocketbase: Client, extractKnownUser: (authStore: CustomAuthStore) => CustomKnownUser = (authStore) => ({ isLoggedIn: true, ...authStore.model } as CustomKnownUser) -): Loadable => { +): Readable => { return readable( pocketbase.authStore.model !== null && pocketbase.authStore.isValid && diff --git a/src/lib/query-key-factory.ts b/src/lib/query-key-factory.ts index 894f63e..b7cba08 100644 --- a/src/lib/query-key-factory.ts +++ b/src/lib/query-key-factory.ts @@ -1,6 +1,14 @@ import type { RecordListQueryParams } from 'pocketbase'; import type Client from 'pocketbase'; +/** + * A collection key factory to generate query keys for TanStack Query based on the parameters given to a Pocketbase `get[...]` function. + * + * @param [options.collection] The Pocketbase collection. + * @param [options.id] The Pocketbase record id. By default, `'*'` to indicate all records in a collection. + * @param [options] The rest of the options are everything that can be passed to `queryParams` parameter in Pocketbase `get[...]` functions. + * @returns A query key, suitable for TanStack Query's `queryKey` option. + */ export const collectionKeys = ({ collection, id = '*', diff --git a/src/lib/types.ts b/src/lib/types.ts index 844f11d..7150ba0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,36 +12,66 @@ import type { QueryKey } from '@tanstack/svelte-query'; +/** + * A known user, used in the `userStore`. + */ export interface KnownUser { isLoggedIn: true; } +/** + * An unknown user or logged in Admin, used in the `userStore`. + */ export interface UnknownUser { isLoggedIn: false; } +/** + * Type-narrowing to differentiate between `Record` and `Admin`. + */ export const isRecord = (test: Record | Admin | null): test is Record => test !== null && 'collectionId' in test && 'collectionName' in test && 'expand' in test; -export interface InfiniteQueryPrefetchOptions< +/** + * Interface for the options parameter in `createRecordQueryPrefetch`. + */ +export interface QueryPrefetchOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> extends Omit, 'queryFn'> { + queryParams?: RecordQueryParams; +} + +/** + * Interface for the options parameter in `createCollectionQueryPrefetch`. + */ +export interface CollectionQueryPrefetchOptions< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey > extends QueryPrefetchOptions { - page?: number; - perPage?: number; + queryParams?: RecordListQueryParams; } -export interface QueryPrefetchOptions< +/** + * Interface for the options parameter in `infiniteCollectionQueryPrefetch`. + */ +export interface InfiniteQueryPrefetchOptions< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey -> extends Omit, 'queryFn'> { - queryParams?: RecordQueryParams; +> extends CollectionQueryPrefetchOptions { + page?: number; + perPage?: number; } +/** + * Interface for the options parameter in `createRecordQuery`. + */ export interface RecordStoreOptions< TQueryFnData = unknown, TError = unknown, @@ -65,8 +95,12 @@ export interface RecordStoreOptions< * This callback will fire any time the realtime subscription receives an update. */ onRealtimeUpdate?: (data: TRealtimeUpdate) => void; + debug?: boolean; } +/** + * Interface for the options parameter in `createCollectionQuery`. + */ export interface CollectionStoreOptions< TQueryFnData = unknown, TError = unknown, @@ -85,6 +119,9 @@ export interface CollectionStoreOptions< filterFunctionThisArg?: any; } +/** + * Interface for the options parameter in `createInfiniteCollectionQuery`. + */ export interface InfiniteCollectionStoreOptions< TQueryFnData = unknown, TError = unknown, @@ -110,4 +147,5 @@ export interface InfiniteCollectionStoreOptions< array: TQueryFnDataSingular[] ) => boolean; filterFunctionThisArg?: any; + debug?: boolean; } diff --git a/vite.config.ts b/vite.config.ts index 313c846..1695034 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,10 +2,7 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; const config: UserConfig = { - plugins: [sveltekit()], - test: { - include: ['src/**/*.{test,spec}.{js,ts}'] - } + plugins: [sveltekit()] }; export default config;