@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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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
|
-
* -
|
|
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",
|