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}
+
+{: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}
+
+ 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, with some_field expanded, sorted by date created (descending), and
+ filtered by date created after 2022-01-01 00:00:00:
+
+{/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;