@ttt-productions/query-core 0.14.6 → 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/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { WithId, FirestoreSourceState, WithSourceState, BatchFirestoreDocOu
4
4
  export { docWithId } from './firestore/types.js';
5
5
  export { flattenInfiniteData, getInfiniteDataCount, } from './firestore/infinite-helpers.js';
6
6
  export type { FirestoreSearchConfig, FirestoreSearchOptions, SearchEqualityFilter, } from './search/types.js';
7
+ export { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';
7
8
  export { createDomainEventInvalidator, exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./domain-events/index.js";
8
9
  export type { DomainEventInvalidator, DomainEventInvalidationRegistry, CacheInvalidation, RefetchType, } from "./domain-events/index.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EACV,MAAM,EACN,oBAAoB,EACpB,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAGzC,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,EAC/B,iBAAiB,EACjB,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EACV,MAAM,EACN,oBAAoB,EACpB,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAGzC,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,EAC/B,iBAAiB,EACjB,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { STALE_TIMES } from './defaults.js';
4
4
  export { invalidateByPrefix, removeByPrefix, updateQueryData, paginatedPageKey, mapPaginatedItems, prependToFirstPaginatedPage, } from './cache-helpers.js';
5
5
  export { docWithId } from './firestore/types.js';
6
6
  export { flattenInfiniteData, getInfiniteDataCount, } from './firestore/infinite-helpers.js';
7
+ export { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';
7
8
  // Domain-event invalidator (mechanism — consumers register their event registry)
8
9
  export { createDomainEventInvalidator, exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./domain-events/index.js";
9
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAmB5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AASzC,iFAAiF;AACjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC","sourcesContent":["// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)\nexport { STALE_TIMES } from './defaults.js';\n\n// Cache Helpers\nexport {\n invalidateByPrefix,\n removeByPrefix,\n updateQueryData,\n paginatedPageKey,\n mapPaginatedItems,\n prependToFirstPaginatedPage,\n} from './cache-helpers.js';\n\n// Firestore Types & Pure Helpers\nexport type {\n WithId,\n FirestoreSourceState,\n WithSourceState,\n BatchFirestoreDocOutcome,\n FirestoreBaseOptions,\n FirestoreDocOptions,\n FirestoreCollectionOptions,\n FirestoreInfiniteOptions,\n FirestorePaginatedOptions,\n InfinitePage,\n PaginatedPage,\n MutationOperation,\n FirestoreMutationOptions,\n FirestoreBatchOptions,\n} from './firestore/types.js';\nexport { docWithId } from './firestore/types.js';\nexport {\n flattenInfiniteData,\n getInfiniteDataCount,\n} from './firestore/infinite-helpers.js';\n\n// Search Types\nexport type {\n FirestoreSearchConfig,\n FirestoreSearchOptions,\n SearchEqualityFilter,\n} from './search/types.js';\n\n// Domain-event invalidator (mechanism — consumers register their event registry)\nexport {\n createDomainEventInvalidator,\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./domain-events/index.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n CacheInvalidation,\n RefetchType,\n} from \"./domain-events/index.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAmB5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAQzC,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEjF,iFAAiF;AACjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC","sourcesContent":["// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)\nexport { STALE_TIMES } from './defaults.js';\n\n// Cache Helpers\nexport {\n invalidateByPrefix,\n removeByPrefix,\n updateQueryData,\n paginatedPageKey,\n mapPaginatedItems,\n prependToFirstPaginatedPage,\n} from './cache-helpers.js';\n\n// Firestore Types & Pure Helpers\nexport type {\n WithId,\n FirestoreSourceState,\n WithSourceState,\n BatchFirestoreDocOutcome,\n FirestoreBaseOptions,\n FirestoreDocOptions,\n FirestoreCollectionOptions,\n FirestoreInfiniteOptions,\n FirestorePaginatedOptions,\n InfinitePage,\n PaginatedPage,\n MutationOperation,\n FirestoreMutationOptions,\n FirestoreBatchOptions,\n} from './firestore/types.js';\nexport { docWithId } from './firestore/types.js';\nexport {\n flattenInfiniteData,\n getInfiniteDataCount,\n} from './firestore/infinite-helpers.js';\n\n// Search Types\nexport type {\n FirestoreSearchConfig,\n FirestoreSearchOptions,\n SearchEqualityFilter,\n} from './search/types.js';\nexport { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText } from './search/rule.js';\n\n// Domain-event invalidator (mechanism — consumers register their event registry)\nexport {\n createDomainEventInvalidator,\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./domain-events/index.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n CacheInvalidation,\n RefetchType,\n} from \"./domain-events/index.js\";\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
- * - Minimum 3 character requirement
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;AAMvD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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,CAiDhE"}
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
- * - Minimum 3 character requirement
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
- // Normalize query text
48
- const normalizedQuery = debouncedQuery.toLowerCase().trim();
49
- const shouldSearch = normalizedQuery.length >= 3 && enabled;
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":"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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';\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 * - Minimum 3 character requirement\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 // Normalize query text\n const normalizedQuery = debouncedQuery.toLowerCase().trim();\n const shouldSearch = normalizedQuery.length >= 3 && enabled;\n\n return 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"]}
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.6",
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",