@ttt-productions/query-core 0.14.5 → 0.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache-helpers.d.ts +17 -0
- package/dist/cache-helpers.d.ts.map +1 -1
- package/dist/cache-helpers.js +48 -0
- package/dist/cache-helpers.js.map +1 -1
- package/dist/firestore/types.d.ts +10 -4
- package/dist/firestore/types.d.ts.map +1 -1
- package/dist/firestore/types.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react/firestore/subscription-status.d.ts +21 -0
- package/dist/react/firestore/subscription-status.d.ts.map +1 -0
- package/dist/react/firestore/subscription-status.js +29 -0
- package/dist/react/firestore/subscription-status.js.map +1 -0
- package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-collection.js +10 -13
- package/dist/react/firestore/use-firestore-collection.js.map +1 -1
- package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-doc.js +10 -13
- package/dist/react/firestore/use-firestore-doc.js.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.d.ts +31 -14
- package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.js +192 -45
- package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.d.ts +27 -24
- package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.js +102 -92
- package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
- package/dist/react/search/use-firestore-search.d.ts +3 -1
- package/dist/react/search/use-firestore-search.d.ts.map +1 -1
- package/dist/react/search/use-firestore-search.js +50 -5
- package/dist/react/search/use-firestore-search.js.map +1 -1
- package/dist/search/rule.d.ts +10 -0
- package/dist/search/rule.d.ts.map +1 -0
- package/dist/search/rule.js +14 -0
- package/dist/search/rule.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,122 +1,132 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useState, useCallback,
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { collection, query, getDocs, limit, startAfter, } from 'firebase/firestore';
|
|
5
5
|
import { useFirestoreDb } from './context.js';
|
|
6
|
+
import { paginatedPageKey } from '../../cache-helpers.js';
|
|
6
7
|
const DEFAULT_PAGE_SIZE = 10;
|
|
7
|
-
const MAX_CACHED_CURSORS = 20;
|
|
8
8
|
/**
|
|
9
|
-
* Page-based pagination for Firestore collections.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Page-based pagination for Firestore collections (Previous / Next, one page at a time).
|
|
10
|
+
*
|
|
11
|
+
* Each page is ONE cache entry at `[...queryKey, 'page', n]` holding its rows, its last
|
|
12
|
+
* row as the next page's cursor, and whether a further page exists — read with a one-row
|
|
13
|
+
* look-ahead, so `hasNextPage` is false exactly when nothing follows the displayed page.
|
|
14
|
+
* Page n reads page n-1's cursor from the cache (loading the chain first if it is
|
|
15
|
+
* missing) and records it as `after`. A displayed page whose `after` no longer matches
|
|
16
|
+
* page n-1's cached cursor (page n-1 was re-read and its last row moved) is re-read before
|
|
17
|
+
* it is shown, so every instance, remount, and refetch of a key agrees on where a page
|
|
18
|
+
* starts. The page number belongs to the query identity (path, key, constraints, page
|
|
19
|
+
* size): a new identity renders `initialPage` in its first render.
|
|
20
|
+
*
|
|
21
|
+
* `queryKey` must encode everything that changes the read — path, filters, ordering, and
|
|
22
|
+
* page size — because the cache entries are shared by key.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
12
25
|
* ```tsx
|
|
13
|
-
* const {
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* hasPrevPage,
|
|
19
|
-
* isLoading,
|
|
20
|
-
* } = useFirestorePaginated<Task>({
|
|
21
|
-
* collectionPath: 'tasks',
|
|
22
|
-
* queryKey: ['tasks', 'list'],
|
|
23
|
-
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
24
|
-
* pageSize: 10,
|
|
26
|
+
* const { data, page, nextPage, prevPage, hasNextPage, hasPrevPage } = useFirestorePaginated<Task>({
|
|
27
|
+
* collectionPath: 'tasks',
|
|
28
|
+
* queryKey: ['tasks', 'active', { pageSize: 10 }],
|
|
29
|
+
* constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],
|
|
30
|
+
* pageSize: 10,
|
|
25
31
|
* });
|
|
26
|
-
* * // Render pagination controls
|
|
27
|
-
* <button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>Previous</button>
|
|
28
|
-
* <span>Page {page}</span>
|
|
29
|
-
* <button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>Next</button>
|
|
30
32
|
* ```
|
|
31
33
|
*/
|
|
32
|
-
export function useFirestorePaginated({ collectionPath, queryKey, constraints = [], pageSize = DEFAULT_PAGE_SIZE, initialPage = 1, enabled = true, staleTime, gcTime, select, }) {
|
|
34
|
+
export function useFirestorePaginated({ collectionPath, queryKey, constraints = [], pageSize = DEFAULT_PAGE_SIZE, initialPage = 1, enabled = true, staleTime, gcTime, refetchInterval, select, }) {
|
|
33
35
|
const db = useFirestoreDb();
|
|
34
36
|
const queryClient = useQueryClient();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...(cursor ? [startAfter(cursor)] : []),
|
|
50
|
-
limit(pageSize),
|
|
51
|
-
];
|
|
52
|
-
const q = query(collectionRef, ...queryConstraints);
|
|
53
|
-
const snapshot = await getDocs(q);
|
|
54
|
-
// Store the last doc as cursor for next page
|
|
55
|
-
if (snapshot.docs.length > 0) {
|
|
56
|
-
const lastDoc = snapshot.docs[snapshot.docs.length - 1];
|
|
57
|
-
setCursors((prev) => {
|
|
58
|
-
const next = new Map(prev);
|
|
59
|
-
next.set(page, lastDoc);
|
|
60
|
-
// Prune old cursors if too many (prevent memory leaks)
|
|
61
|
-
if (next.size > MAX_CACHED_CURSORS) {
|
|
62
|
-
const sorted = Array.from(next.keys()).sort((a, b) => a - b);
|
|
63
|
-
const toDelete = sorted.slice(0, sorted.length - MAX_CACHED_CURSORS);
|
|
64
|
-
toDelete.forEach((k) => next.delete(k));
|
|
65
|
-
}
|
|
66
|
-
return next;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
// Determine if there are more pages
|
|
70
|
-
setHasMore(snapshot.docs.length === pageSize);
|
|
71
|
-
return snapshot.docs.map((docSnap) => {
|
|
72
|
-
const rawData = docSnap.data();
|
|
73
|
-
// Include id in data passed to select, so it can be renamed/transformed
|
|
74
|
-
const dataWithId = { id: docSnap.id, ...rawData };
|
|
75
|
-
const data = select ? select(dataWithId) : dataWithId;
|
|
76
|
-
return data;
|
|
37
|
+
const queryKeyMemo = JSON.stringify(queryKey);
|
|
38
|
+
const constraintsMemo = JSON.stringify(constraints);
|
|
39
|
+
const identity = JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, pageSize]);
|
|
40
|
+
const firstPage = Math.max(1, Math.floor(initialPage));
|
|
41
|
+
const [pageState, setPageState] = useState(() => ({ identity, page: firstPage }));
|
|
42
|
+
const page = pageState.identity === identity ? pageState.page : firstPage;
|
|
43
|
+
const selectRef = useRef(select);
|
|
44
|
+
selectRef.current = select;
|
|
45
|
+
const fetchPage = useCallback(async (n) => {
|
|
46
|
+
let cursor = null;
|
|
47
|
+
if (n > 1) {
|
|
48
|
+
const previous = await queryClient.ensureQueryData({
|
|
49
|
+
queryKey: paginatedPageKey(queryKey, n - 1),
|
|
50
|
+
queryFn: () => fetchPage(n - 1),
|
|
77
51
|
});
|
|
78
|
-
|
|
52
|
+
if (!previous.hasMore || !previous.cursor) {
|
|
53
|
+
return { items: [], cursor: null, hasMore: false, after: previous.cursor?.id ?? null };
|
|
54
|
+
}
|
|
55
|
+
cursor = previous.cursor;
|
|
56
|
+
}
|
|
57
|
+
const q = query(collection(db, collectionPath), ...constraints, ...(cursor ? [startAfter(cursor)] : []), limit(pageSize + 1));
|
|
58
|
+
const snapshot = await getDocs(q);
|
|
59
|
+
const pageDocs = snapshot.docs.slice(0, pageSize);
|
|
60
|
+
return {
|
|
61
|
+
items: pageDocs.map((docSnap) => {
|
|
62
|
+
const dataWithId = { id: docSnap.id, ...docSnap.data() };
|
|
63
|
+
const mapped = selectRef.current ? selectRef.current(dataWithId) : dataWithId;
|
|
64
|
+
return mapped;
|
|
65
|
+
}),
|
|
66
|
+
cursor: pageDocs[pageDocs.length - 1] ?? null,
|
|
67
|
+
hasMore: snapshot.docs.length > pageSize,
|
|
68
|
+
after: cursor?.id ?? null,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
// queryKey and constraints are captured through their serialized identity.
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
[db, queryClient, identity, selectRef]);
|
|
74
|
+
const queryResult = useQuery({
|
|
75
|
+
queryKey: paginatedPageKey(queryKey, page),
|
|
76
|
+
queryFn: () => fetchPage(page),
|
|
79
77
|
enabled,
|
|
80
78
|
staleTime,
|
|
81
79
|
gcTime,
|
|
80
|
+
refetchInterval,
|
|
82
81
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
// The page before the displayed one, observed from the cache only (never fetched here).
|
|
83
|
+
// When it is re-read and its last row moves, the displayed entry no longer starts where the
|
|
84
|
+
// chain says it must: it is re-read before it is shown as current.
|
|
85
|
+
const previousPage = useQuery({
|
|
86
|
+
queryKey: paginatedPageKey(queryKey, Math.max(1, page - 1)),
|
|
87
|
+
queryFn: () => fetchPage(Math.max(1, page - 1)),
|
|
88
|
+
enabled: false,
|
|
89
|
+
});
|
|
90
|
+
const cachedEntry = queryResult.data;
|
|
91
|
+
const outOfChain = page > 1 &&
|
|
92
|
+
cachedEntry !== undefined &&
|
|
93
|
+
(previousPage.data === undefined || cachedEntry.after !== (previousPage.data.cursor?.id ?? null));
|
|
94
|
+
const { refetch, isFetching } = queryResult;
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (enabled && outOfChain && !isFetching)
|
|
97
|
+
void refetch();
|
|
98
|
+
}, [enabled, outOfChain, isFetching, refetch]);
|
|
99
|
+
const entry = outOfChain ? undefined : cachedEntry;
|
|
100
|
+
const hasNextPage = Boolean(entry?.hasMore && entry.cursor);
|
|
101
|
+
const setPage = useCallback((target) => {
|
|
102
|
+
const next = Math.floor(target);
|
|
103
|
+
if (next < 1 || next === page)
|
|
89
104
|
return;
|
|
105
|
+
if (next > page + 1) {
|
|
106
|
+
const before = queryClient.getQueryData(paginatedPageKey(queryKey, next - 1));
|
|
107
|
+
if (!before?.hasMore)
|
|
108
|
+
return;
|
|
90
109
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, [page, setPage]);
|
|
101
|
-
const resetToFirstPage = useCallback(() => {
|
|
102
|
-
setCursors(new Map());
|
|
103
|
-
setPageInternal(1);
|
|
104
|
-
setHasMore(true);
|
|
105
|
-
// Invalidate all cached pages
|
|
106
|
-
queryClient.invalidateQueries({ queryKey, exact: false });
|
|
107
|
-
}, [queryClient, queryKey]);
|
|
110
|
+
if (next === page + 1 && !hasNextPage)
|
|
111
|
+
return;
|
|
112
|
+
setPageState({ identity, page: next });
|
|
113
|
+
},
|
|
114
|
+
// queryKey is captured through its serialized identity.
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
[page, hasNextPage, identity, queryClient]);
|
|
117
|
+
const nextPage = useCallback(() => setPage(page + 1), [page, setPage]);
|
|
118
|
+
const prevPage = useCallback(() => setPage(page - 1), [page, setPage]);
|
|
108
119
|
return {
|
|
109
120
|
...queryResult,
|
|
110
|
-
|
|
121
|
+
...(outOfChain ? { isLoading: true, isPending: true, isSuccess: false, status: 'pending' } : {}),
|
|
122
|
+
data: entry?.items,
|
|
111
123
|
page,
|
|
112
124
|
pageSize,
|
|
113
|
-
hasNextPage
|
|
125
|
+
hasNextPage,
|
|
114
126
|
hasPrevPage: page > 1,
|
|
115
|
-
// Navigation functions
|
|
116
127
|
setPage,
|
|
117
128
|
nextPage,
|
|
118
129
|
prevPage,
|
|
119
|
-
resetToFirstPage,
|
|
120
130
|
};
|
|
121
131
|
}
|
|
122
132
|
//# sourceMappingURL=use-firestore-paginated.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-paginated.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useCallback, useMemo } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n limit,\n startAfter,\n type DocumentData,\n type DocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport type { FirestorePaginatedOptions, WithId } from '../../firestore/types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\nconst MAX_CACHED_CURSORS = 20;\n\n/**\n * Return type for useFirestorePaginated hook\n */\nexport type UseFirestorePaginatedResult<T> = UseQueryResult<WithId<T>[], Error> & {\n page: number;\n pageSize: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n setPage: (page: number) => void;\n nextPage: () => void;\n prevPage: () => void;\n resetToFirstPage: () => void;\n};\n\n/**\n * Page-based pagination for Firestore collections.\n * Supports navigating to specific pages (1, 2, 3...) rather than infinite scroll.\n * * @example\n * ```tsx\n * const {\n * data,\n * page,\n * setPage,\n * hasNextPage,\n * hasPrevPage,\n * isLoading,\n * } = useFirestorePaginated<Task>({\n * collectionPath: 'tasks',\n * queryKey: ['tasks', 'list'],\n * constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],\n * pageSize: 10,\n * });\n * * // Render pagination controls\n * <button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>Previous</button>\n * <span>Page {page}</span>\n * <button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>Next</button>\n * ```\n */\nexport function useFirestorePaginated<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n pageSize = DEFAULT_PAGE_SIZE,\n initialPage = 1,\n enabled = true,\n staleTime,\n gcTime,\n select,\n}: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n const [page, setPageInternal] = useState(initialPage);\n const [cursors, setCursors] = useState<Map<number, DocumentSnapshot>>(new Map());\n const [hasMore, setHasMore] = useState(true);\n\n // Create a stable query key that includes the page\n const pageQueryKey = useMemo(\n () => [...queryKey, 'page', page],\n [queryKey, page]\n );\n\n const queryResult = useQuery({\n queryKey: pageQueryKey,\n queryFn: async (): Promise<WithId<T>[]> => {\n const collectionRef = collection(db, collectionPath);\n\n // Get cursor for current page (if not page 1)\n const cursor = page > 1 ? cursors.get(page - 1) : undefined;\n\n // Build query\n const queryConstraints = [\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(pageSize),\n ];\n\n const q = query(collectionRef, ...queryConstraints);\n const snapshot = await getDocs(q);\n\n // Store the last doc as cursor for next page\n if (snapshot.docs.length > 0) {\n const lastDoc = snapshot.docs[snapshot.docs.length - 1];\n setCursors((prev) => {\n const next = new Map(prev);\n next.set(page, lastDoc);\n\n // Prune old cursors if too many (prevent memory leaks)\n if (next.size > MAX_CACHED_CURSORS) {\n const sorted = Array.from(next.keys()).sort((a, b) => a - b);\n const toDelete = sorted.slice(0, sorted.length - MAX_CACHED_CURSORS);\n toDelete.forEach((k) => next.delete(k));\n }\n\n return next;\n });\n }\n\n // Determine if there are more pages\n setHasMore(snapshot.docs.length === pageSize);\n\n return snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n // Include id in data passed to select, so it can be renamed/transformed\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n },\n enabled,\n staleTime,\n gcTime,\n });\n\n const setPage = useCallback((newPage: number) => {\n if (newPage < 1) return;\n\n // Can only go forward one page at a time if cursor doesn't exist\n if (newPage > page + 1 && !cursors.has(newPage - 1)) {\n console.warn('[useFirestorePaginated] Cannot skip pages. Navigate sequentially.');\n return;\n }\n\n setPageInternal(newPage);\n }, [page, cursors]);\n\n const nextPage = useCallback(() => {\n if (hasMore) setPage(page + 1);\n }, [page, hasMore, setPage]);\n\n const prevPage = useCallback(() => {\n if (page > 1) setPage(page - 1);\n }, [page, setPage]);\n\n const resetToFirstPage = useCallback(() => {\n setCursors(new Map());\n setPageInternal(1);\n setHasMore(true);\n // Invalidate all cached pages\n queryClient.invalidateQueries({ queryKey, exact: false });\n }, [queryClient, queryKey]);\n\n return {\n ...queryResult,\n // Pagination state\n page,\n pageSize,\n hasNextPage: hasMore,\n hasPrevPage: page > 1,\n // Navigation functions\n setPage,\n nextPage,\n prevPage,\n resetToFirstPage,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-firestore-paginated.js","sourceRoot":"","sources":["../../../src/react/firestore/use-firestore-paginated.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n query,\n getDocs,\n limit,\n startAfter,\n type DocumentData,\n type DocumentSnapshot,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from './context.js';\nimport { paginatedPageKey } from '../../cache-helpers.js';\nimport type { FirestorePaginatedOptions, PaginatedPage, WithId } from '../../firestore/types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\n\n/**\n * Return type for useFirestorePaginated hook. `data` is the displayed page's rows; the\n * page's cache entry (rows, cursor, has-more) lives at `paginatedPageKey(queryKey, page)`.\n */\nexport type UseFirestorePaginatedResult<T> = Omit<UseQueryResult<PaginatedPage<WithId<T>>, Error>, 'data'> & {\n data: WithId<T>[] | undefined;\n page: number;\n pageSize: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n setPage: (page: number) => void;\n nextPage: () => void;\n prevPage: () => void;\n};\n\n/**\n * Page-based pagination for Firestore collections (Previous / Next, one page at a time).\n *\n * Each page is ONE cache entry at `[...queryKey, 'page', n]` holding its rows, its last\n * row as the next page's cursor, and whether a further page exists — read with a one-row\n * look-ahead, so `hasNextPage` is false exactly when nothing follows the displayed page.\n * Page n reads page n-1's cursor from the cache (loading the chain first if it is\n * missing) and records it as `after`. A displayed page whose `after` no longer matches\n * page n-1's cached cursor (page n-1 was re-read and its last row moved) is re-read before\n * it is shown, so every instance, remount, and refetch of a key agrees on where a page\n * starts. The page number belongs to the query identity (path, key, constraints, page\n * size): a new identity renders `initialPage` in its first render.\n *\n * `queryKey` must encode everything that changes the read — path, filters, ordering, and\n * page size — because the cache entries are shared by key.\n *\n * @example\n * ```tsx\n * const { data, page, nextPage, prevPage, hasNextPage, hasPrevPage } = useFirestorePaginated<Task>({\n * collectionPath: 'tasks',\n * queryKey: ['tasks', 'active', { pageSize: 10 }],\n * constraints: [where('status', '==', 'active'), orderBy('createdAt', 'desc')],\n * pageSize: 10,\n * });\n * ```\n */\nexport function useFirestorePaginated<T extends DocumentData = DocumentData>({\n collectionPath,\n queryKey,\n constraints = [],\n pageSize = DEFAULT_PAGE_SIZE,\n initialPage = 1,\n enabled = true,\n staleTime,\n gcTime,\n refetchInterval,\n select,\n}: FirestorePaginatedOptions<T>): UseFirestorePaginatedResult<T> {\n const db = useFirestoreDb();\n const queryClient = useQueryClient();\n\n const queryKeyMemo = JSON.stringify(queryKey);\n const constraintsMemo = JSON.stringify(constraints);\n const identity = JSON.stringify([collectionPath, queryKeyMemo, constraintsMemo, pageSize]);\n const firstPage = Math.max(1, Math.floor(initialPage));\n\n const [pageState, setPageState] = useState(() => ({ identity, page: firstPage }));\n const page = pageState.identity === identity ? pageState.page : firstPage;\n\n const selectRef = useRef(select);\n selectRef.current = select;\n\n const fetchPage = useCallback(\n async (n: number): Promise<PaginatedPage<WithId<T>>> => {\n let cursor: DocumentSnapshot | null = null;\n if (n > 1) {\n const previous = await queryClient.ensureQueryData<PaginatedPage<WithId<T>>>({\n queryKey: paginatedPageKey(queryKey, n - 1),\n queryFn: () => fetchPage(n - 1),\n });\n if (!previous.hasMore || !previous.cursor) {\n return { items: [], cursor: null, hasMore: false, after: previous.cursor?.id ?? null };\n }\n cursor = previous.cursor;\n }\n const q = query(\n collection(db, collectionPath),\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(pageSize + 1),\n );\n const snapshot = await getDocs(q);\n const pageDocs = snapshot.docs.slice(0, pageSize);\n return {\n items: pageDocs.map((docSnap) => {\n const dataWithId = { id: docSnap.id, ...docSnap.data() };\n const mapped = selectRef.current ? selectRef.current(dataWithId) : dataWithId;\n return mapped as WithId<T>;\n }),\n cursor: pageDocs[pageDocs.length - 1] ?? null,\n hasMore: snapshot.docs.length > pageSize,\n after: cursor?.id ?? null,\n };\n },\n // queryKey and constraints are captured through their serialized identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [db, queryClient, identity, selectRef],\n );\n\n const queryResult = useQuery({\n queryKey: paginatedPageKey(queryKey, page),\n queryFn: () => fetchPage(page),\n enabled,\n staleTime,\n gcTime,\n refetchInterval,\n });\n\n // The page before the displayed one, observed from the cache only (never fetched here).\n // When it is re-read and its last row moves, the displayed entry no longer starts where the\n // chain says it must: it is re-read before it is shown as current.\n const previousPage = useQuery({\n queryKey: paginatedPageKey(queryKey, Math.max(1, page - 1)),\n queryFn: () => fetchPage(Math.max(1, page - 1)),\n enabled: false,\n });\n const cachedEntry = queryResult.data;\n const outOfChain =\n page > 1 &&\n cachedEntry !== undefined &&\n (previousPage.data === undefined || cachedEntry.after !== (previousPage.data.cursor?.id ?? null));\n const { refetch, isFetching } = queryResult;\n useEffect(() => {\n if (enabled && outOfChain && !isFetching) void refetch();\n }, [enabled, outOfChain, isFetching, refetch]);\n\n const entry = outOfChain ? undefined : cachedEntry;\n const hasNextPage = Boolean(entry?.hasMore && entry.cursor);\n\n const setPage = useCallback(\n (target: number) => {\n const next = Math.floor(target);\n if (next < 1 || next === page) return;\n if (next > page + 1) {\n const before = queryClient.getQueryData<PaginatedPage<unknown>>(\n paginatedPageKey(queryKey, next - 1),\n );\n if (!before?.hasMore) return;\n }\n if (next === page + 1 && !hasNextPage) return;\n setPageState({ identity, page: next });\n },\n // queryKey is captured through its serialized identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [page, hasNextPage, identity, queryClient],\n );\n\n const nextPage = useCallback(() => setPage(page + 1), [page, setPage]);\n const prevPage = useCallback(() => setPage(page - 1), [page, setPage]);\n\n return {\n ...queryResult,\n ...(outOfChain ? { isLoading: true, isPending: true, isSuccess: false, status: 'pending' as const } : {}),\n data: entry?.items,\n page,\n pageSize,\n hasNextPage,\n hasPrevPage: page > 1,\n setPage,\n nextPage,\n prevPage,\n } as UseFirestorePaginatedResult<T>;\n}\n"]}
|
|
@@ -9,7 +9,9 @@ import type { WithId } from '../../firestore/types.js';
|
|
|
9
9
|
* Features:
|
|
10
10
|
* - Debounced input (default 300ms)
|
|
11
11
|
* - Case-insensitive search
|
|
12
|
-
* -
|
|
12
|
+
* - Searches only a text `isSearchableText` accepts (`FIRESTORE_SEARCH_MIN_LENGTH`, trimmed)
|
|
13
|
+
* - No answer while the debounce lags the typed text: `data` is undefined and `isLoading` is true
|
|
14
|
+
* (idle instead when the typed text is too short or the search is disabled)
|
|
13
15
|
* - Prefix matching with '\uf8ff' suffix
|
|
14
16
|
* - Configurable result limit
|
|
15
17
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-search.d.ts","sourceRoot":"","sources":["../../../src/react/search/use-firestore-search.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAOL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"use-firestore-search.d.ts","sourceRoot":"","sources":["../../../src/react/search/use-firestore-search.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAOL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAOvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,cAAc,EACd,WAAW,EACX,SAAS,EACT,KAAqB,EACrB,OAAc,EACd,MAAM,EACN,UAAgC,EAChC,SAA8B,EAC9B,eAAoB,GACrB,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAwDhE"}
|
|
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
4
4
|
import { collection, getDocs, query, where, orderBy, limit as firestoreLimit, } from 'firebase/firestore';
|
|
5
5
|
import { useFirestoreDb } from '../firestore/context.js';
|
|
6
6
|
import { keys } from '../../keys.js';
|
|
7
|
+
import { isSearchableText, normalizeSearchText } from '../../search/rule.js';
|
|
7
8
|
const DEFAULT_LIMIT = 5;
|
|
8
9
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
9
10
|
const DEFAULT_STALE_TIME = 60 * 1000; // 1 minute
|
|
@@ -14,7 +15,9 @@ const DEFAULT_STALE_TIME = 60 * 1000; // 1 minute
|
|
|
14
15
|
* Features:
|
|
15
16
|
* - Debounced input (default 300ms)
|
|
16
17
|
* - Case-insensitive search
|
|
17
|
-
* -
|
|
18
|
+
* - Searches only a text `isSearchableText` accepts (`FIRESTORE_SEARCH_MIN_LENGTH`, trimmed)
|
|
19
|
+
* - No answer while the debounce lags the typed text: `data` is undefined and `isLoading` is true
|
|
20
|
+
* (idle instead when the typed text is too short or the search is disabled)
|
|
18
21
|
* - Prefix matching with '\uf8ff' suffix
|
|
19
22
|
* - Configurable result limit
|
|
20
23
|
*
|
|
@@ -44,10 +47,9 @@ export function useFirestoreSearch({ collectionPath, searchField, queryText, lim
|
|
|
44
47
|
}, debounceMs);
|
|
45
48
|
return () => clearTimeout(timer);
|
|
46
49
|
}, [queryText, debounceMs]);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
return useQuery({
|
|
50
|
+
const normalizedQuery = normalizeSearchText(debouncedQuery);
|
|
51
|
+
const shouldSearch = enabled && isSearchableText(debouncedQuery);
|
|
52
|
+
const searchQuery = useQuery({
|
|
51
53
|
queryKey: keys.custom('search', collectionPath, searchField, normalizedQuery, JSON.stringify(equalityFilters)),
|
|
52
54
|
queryFn: async () => {
|
|
53
55
|
if (!shouldSearch) {
|
|
@@ -75,5 +77,48 @@ export function useFirestoreSearch({ collectionPath, searchField, queryText, lim
|
|
|
75
77
|
enabled: shouldSearch,
|
|
76
78
|
staleTime,
|
|
77
79
|
});
|
|
80
|
+
// Until the debounce catches up, the query's key holds an earlier text, so its result (or its
|
|
81
|
+
// absence) is not an answer for the text typed now (FRONTEND-206): report no answer instead —
|
|
82
|
+
// still searching when the typed text will be searched, idle when it will not.
|
|
83
|
+
const typedQuery = normalizeSearchText(queryText);
|
|
84
|
+
if (typedQuery === normalizedQuery)
|
|
85
|
+
return searchQuery;
|
|
86
|
+
const willSearch = enabled && isSearchableText(queryText);
|
|
87
|
+
return withoutAnswer(searchQuery, willSearch);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The query result with every answer field replaced. The other fields stay getters onto the
|
|
91
|
+
* original result: TanStack tracks which fields a component reads to decide when it re-renders,
|
|
92
|
+
* and reading them all here (a spread) would opt every consumer out of that.
|
|
93
|
+
*/
|
|
94
|
+
function withoutAnswer(result, searching) {
|
|
95
|
+
const answerFields = {
|
|
96
|
+
data: undefined,
|
|
97
|
+
error: null,
|
|
98
|
+
isError: false,
|
|
99
|
+
isLoadingError: false,
|
|
100
|
+
isRefetchError: false,
|
|
101
|
+
isSuccess: false,
|
|
102
|
+
isPending: true,
|
|
103
|
+
isPlaceholderData: false,
|
|
104
|
+
isFetched: false,
|
|
105
|
+
isFetchedAfterMount: false,
|
|
106
|
+
isRefetching: false,
|
|
107
|
+
isLoading: searching,
|
|
108
|
+
isFetching: searching,
|
|
109
|
+
status: 'pending',
|
|
110
|
+
fetchStatus: searching ? 'fetching' : 'idle',
|
|
111
|
+
dataUpdatedAt: 0,
|
|
112
|
+
errorUpdatedAt: 0,
|
|
113
|
+
};
|
|
114
|
+
const view = {};
|
|
115
|
+
for (const key of Object.keys(result)) {
|
|
116
|
+
Object.defineProperty(view, key, {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true,
|
|
119
|
+
get: key in answerFields ? () => answerFields[key] : () => result[key],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return view;
|
|
78
123
|
}
|
|
79
124
|
//# sourceMappingURL=use-firestore-search.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-firestore-search.js","sourceRoot":"","sources":["../../../src/react/search/use-firestore-search.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAuB,MAAM,uBAAuB,CAAC;AACtE,OAAO,EACL,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,KAAK,IAAI,cAAc,GAGxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"use-firestore-search.js","sourceRoot":"","sources":["../../../src/react/search/use-firestore-search.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useEffect } from 'react';\nimport { useQuery, type UseQueryResult } from '@tanstack/react-query';\nimport {\n collection,\n getDocs,\n query,\n where,\n orderBy,\n limit as firestoreLimit,\n type DocumentData,\n type QueryConstraint,\n} from 'firebase/firestore';\nimport { useFirestoreDb } from '../firestore/context.js';\nimport { keys } from '../../keys.js';\nimport type { FirestoreSearchOptions } from '../../search/types.js';\nimport type { WithId } from '../../firestore/types.js';\nimport { isSearchableText, normalizeSearchText } from '../../search/rule.js';\n\nconst DEFAULT_LIMIT = 5;\nconst DEFAULT_DEBOUNCE_MS = 300;\nconst DEFAULT_STALE_TIME = 60 * 1000; // 1 minute\n\n/**\n * Generic Firestore search hook with debouncing and case-insensitive prefix matching.\n * Automatically normalizes search text to lowercase and uses Firestore range queries.\n *\n * Features:\n * - Debounced input (default 300ms)\n * - Case-insensitive search\n * - Searches only a text `isSearchableText` accepts (`FIRESTORE_SEARCH_MIN_LENGTH`, trimmed)\n * - No answer while the debounce lags the typed text: `data` is undefined and `isLoading` is true\n * (idle instead when the typed text is too short or the search is disabled)\n * - Prefix matching with '\\uf8ff' suffix\n * - Configurable result limit\n *\n * @example\n * ```typescript\n * const { data, isLoading } = useFirestoreSearch<UserProfile>({\n * collectionPath: 'userProfiles',\n * searchField: 'username',\n * queryText: searchValue,\n * });\n *\n * const { data } = useFirestoreSearch<Team>({\n * collectionPath: 'teams',\n * searchField: 'name_lowercase',\n * queryText: searchValue,\n * limit: 10,\n * });\n * ```\n */\nexport function useFirestoreSearch<T extends DocumentData = DocumentData>({\n collectionPath,\n searchField,\n queryText,\n limit = DEFAULT_LIMIT,\n enabled = true,\n select,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n staleTime = DEFAULT_STALE_TIME,\n equalityFilters = [],\n}: FirestoreSearchOptions<T>): UseQueryResult<WithId<T>[], Error> {\n const db = useFirestoreDb();\n const [debouncedQuery, setDebouncedQuery] = useState(queryText);\n\n // Debounce the search query\n useEffect(() => {\n const timer = setTimeout(() => {\n setDebouncedQuery(queryText);\n }, debounceMs);\n\n return () => clearTimeout(timer);\n }, [queryText, debounceMs]);\n\n const normalizedQuery = normalizeSearchText(debouncedQuery);\n const shouldSearch = enabled && isSearchableText(debouncedQuery);\n\n const searchQuery = useQuery({\n queryKey: keys.custom('search', collectionPath, searchField, normalizedQuery, JSON.stringify(equalityFilters)),\n queryFn: async (): Promise<WithId<T>[]> => {\n if (!shouldSearch) {\n return [];\n }\n\n const collectionRef = collection(db, collectionPath);\n\n // Build range query for prefix matching\n // Using '\\uf8ff' as upper bound creates a range that matches all strings starting with the query\n const constraints: QueryConstraint[] = [\n ...equalityFilters.map((f) => where(f.field, '==', f.value)),\n where(searchField, '>=', normalizedQuery),\n where(searchField, '<=', normalizedQuery + '\\uf8ff'),\n orderBy(searchField, 'asc'),\n firestoreLimit(limit),\n ];\n\n const q = query(collectionRef, ...constraints);\n const snapshot = await getDocs(q);\n\n return snapshot.docs.map((docSnap) => {\n const rawData = docSnap.data();\n const dataWithId = { id: docSnap.id, ...rawData };\n const data = select ? select(dataWithId) : dataWithId;\n return data as WithId<T>;\n });\n },\n enabled: shouldSearch,\n staleTime,\n });\n\n // Until the debounce catches up, the query's key holds an earlier text, so its result (or its\n // absence) is not an answer for the text typed now (FRONTEND-206): report no answer instead —\n // still searching when the typed text will be searched, idle when it will not.\n const typedQuery = normalizeSearchText(queryText);\n if (typedQuery === normalizedQuery) return searchQuery;\n const willSearch = enabled && isSearchableText(queryText);\n return withoutAnswer(searchQuery, willSearch);\n}\n\n/**\n * The query result with every answer field replaced. The other fields stay getters onto the\n * original result: TanStack tracks which fields a component reads to decide when it re-renders,\n * and reading them all here (a spread) would opt every consumer out of that.\n */\nfunction withoutAnswer<R extends UseQueryResult<unknown, Error>>(result: R, searching: boolean): R {\n const answerFields: Record<string, unknown> = {\n data: undefined,\n error: null,\n isError: false,\n isLoadingError: false,\n isRefetchError: false,\n isSuccess: false,\n isPending: true,\n isPlaceholderData: false,\n isFetched: false,\n isFetchedAfterMount: false,\n isRefetching: false,\n isLoading: searching,\n isFetching: searching,\n status: 'pending',\n fetchStatus: searching ? 'fetching' : 'idle',\n dataUpdatedAt: 0,\n errorUpdatedAt: 0,\n };\n const view = {} as R;\n for (const key of Object.keys(result)) {\n Object.defineProperty(view, key, {\n enumerable: true,\n configurable: true,\n get: key in answerFields ? () => answerFields[key] : () => result[key as keyof R],\n });\n }\n return view;\n}\n\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The fewest characters, counted on the normalized text, that `useFirestoreSearch` searches. */
|
|
2
|
+
export declare const FIRESTORE_SEARCH_MIN_LENGTH = 3;
|
|
3
|
+
/**
|
|
4
|
+
* The text `useFirestoreSearch` reads for a typed text: lowercased, surrounding whitespace dropped.
|
|
5
|
+
* Two typed texts with the same normalized text are the same search.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeSearchText(text: string): string;
|
|
8
|
+
/** Whether `useFirestoreSearch` searches this typed text (its normalized length reaches the minimum). */
|
|
9
|
+
export declare function isSearchableText(text: string): boolean;
|
|
10
|
+
//# sourceMappingURL=rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/search/rule.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The fewest characters, counted on the normalized text, that `useFirestoreSearch` searches. */
|
|
2
|
+
export const FIRESTORE_SEARCH_MIN_LENGTH = 3;
|
|
3
|
+
/**
|
|
4
|
+
* The text `useFirestoreSearch` reads for a typed text: lowercased, surrounding whitespace dropped.
|
|
5
|
+
* Two typed texts with the same normalized text are the same search.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeSearchText(text) {
|
|
8
|
+
return text.toLowerCase().trim();
|
|
9
|
+
}
|
|
10
|
+
/** Whether `useFirestoreSearch` searches this typed text (its normalized length reaches the minimum). */
|
|
11
|
+
export function isSearchableText(text) {
|
|
12
|
+
return normalizeSearchText(text).length >= FIRESTORE_SEARCH_MIN_LENGTH;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/search/rule.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,2BAA2B,CAAC;AACzE,CAAC","sourcesContent":["/** The fewest characters, counted on the normalized text, that `useFirestoreSearch` searches. */\nexport const FIRESTORE_SEARCH_MIN_LENGTH = 3;\n\n/**\n * The text `useFirestoreSearch` reads for a typed text: lowercased, surrounding whitespace dropped.\n * Two typed texts with the same normalized text are the same search.\n */\nexport function normalizeSearchText(text: string): string {\n return text.toLowerCase().trim();\n}\n\n/** Whether `useFirestoreSearch` searches this typed text (its normalized length reaches the minimum). */\nexport function isSearchableText(text: string): boolean {\n return normalizeSearchText(text).length >= FIRESTORE_SEARCH_MIN_LENGTH;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/query-core",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.7",
|
|
4
4
|
"description": "Shared TanStack Query (React Query) client defaults, query key helpers, and Firestore integration hooks for TTT Productions apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|