@uniformdev/search 0.0.2 → 0.0.3

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.
@@ -0,0 +1,99 @@
1
+ type SearchCollection = 'entries' | 'compositions' | 'assets';
2
+ type SearchResponseHighlightObject = {
3
+ snippet?: string;
4
+ snippets?: string[];
5
+ value?: unknown;
6
+ values?: unknown[];
7
+ matched_tokens?: unknown;
8
+ };
9
+ type SearchResponseHighlight = {
10
+ [field: string]: SearchResponseHighlight | SearchResponseHighlightObject;
11
+ };
12
+ type SearchHit = {
13
+ id: string;
14
+ _collection: SearchCollection;
15
+ /** Per-field highlights (nested by field name). Present only when there are matches. */
16
+ _highlight?: SearchResponseHighlight;
17
+ /** Per-field highlights (array form, each entry carries a `field`). */
18
+ _highlights?: unknown[];
19
+ [key: string]: unknown;
20
+ };
21
+ type Pagination<Item> = {
22
+ items: Item[];
23
+ page: number;
24
+ perPage: number;
25
+ total: number;
26
+ totalPages: number;
27
+ };
28
+ type CollectionResult = {
29
+ data: Pagination<SearchHit>;
30
+ facets: Facets;
31
+ };
32
+ type FilterByItem = {
33
+ title: string;
34
+ value: string;
35
+ };
36
+ type FacetBy = {
37
+ type: 'select' | 'multiSelect' | 'range';
38
+ title: string;
39
+ fieldKey: string;
40
+ };
41
+ type OrderBy = {
42
+ title: string;
43
+ field: string;
44
+ direction: 'ASC' | 'DESC';
45
+ };
46
+ type PageSize = {
47
+ size: number;
48
+ /** When set, this size is the initial page size (first `default` wins). */
49
+ default?: boolean;
50
+ };
51
+ type Facets = {
52
+ [key: string]: {
53
+ [key: string]: number;
54
+ };
55
+ };
56
+ type FilterQuery = {
57
+ [K: string]: string[];
58
+ };
59
+ type SearchItemConfig = {
60
+ source: string;
61
+ type: string;
62
+ nodeId?: string;
63
+ urlTemplate?: string;
64
+ tokenMapping?: Record<string, string>;
65
+ };
66
+
67
+ interface SearchParams {
68
+ projectId?: string;
69
+ page?: number;
70
+ perPage?: number;
71
+ filters?: Record<string, unknown>;
72
+ baseFilterBy?: string;
73
+ facetBy?: string;
74
+ queryBy?: string;
75
+ search?: string;
76
+ orderBy?: string;
77
+ locale?: string;
78
+ maxFacetValues?: number;
79
+ /** Max typos tolerated per word: 0, 1, or 2. (Typesense `num_typos`.) */
80
+ maxTypos?: number;
81
+ /** Minimum word length before a single typo is allowed. (Typesense `min_len_1typo`.) */
82
+ minLengthFor1Typo?: number;
83
+ /** Minimum word length before two typos are allowed. (Typesense `min_len_2typo`.) */
84
+ minLengthFor2Typos?: number;
85
+ /** If a query returns fewer than this many results, retry with more typos. (Typesense `typo_tokens_threshold`.) */
86
+ typoFallbackThreshold?: number;
87
+ /** Rank exact matches above typo-corrected ones. (Typesense `prioritize_exact_match`.) */
88
+ prioritizeExactMatch?: boolean;
89
+ }
90
+ interface SearchClient {
91
+ performSearch(params: SearchParams): Promise<CollectionResult>;
92
+ }
93
+ declare function createSearchClient(config: {
94
+ apiUrl: string;
95
+ apiKey?: string;
96
+ projectId?: string;
97
+ }): SearchClient;
98
+
99
+ export { type CollectionResult as C, type FacetBy as F, type OrderBy as O, type PageSize as P, type SearchHit as S, type Facets as a, type FilterByItem as b, type FilterQuery as c, type Pagination as d, type SearchClient as e, type SearchCollection as f, type SearchItemConfig as g, type SearchParams as h, type SearchResponseHighlight as i, type SearchResponseHighlightObject as j, createSearchClient as k };
@@ -0,0 +1,99 @@
1
+ type SearchCollection = 'entries' | 'compositions' | 'assets';
2
+ type SearchResponseHighlightObject = {
3
+ snippet?: string;
4
+ snippets?: string[];
5
+ value?: unknown;
6
+ values?: unknown[];
7
+ matched_tokens?: unknown;
8
+ };
9
+ type SearchResponseHighlight = {
10
+ [field: string]: SearchResponseHighlight | SearchResponseHighlightObject;
11
+ };
12
+ type SearchHit = {
13
+ id: string;
14
+ _collection: SearchCollection;
15
+ /** Per-field highlights (nested by field name). Present only when there are matches. */
16
+ _highlight?: SearchResponseHighlight;
17
+ /** Per-field highlights (array form, each entry carries a `field`). */
18
+ _highlights?: unknown[];
19
+ [key: string]: unknown;
20
+ };
21
+ type Pagination<Item> = {
22
+ items: Item[];
23
+ page: number;
24
+ perPage: number;
25
+ total: number;
26
+ totalPages: number;
27
+ };
28
+ type CollectionResult = {
29
+ data: Pagination<SearchHit>;
30
+ facets: Facets;
31
+ };
32
+ type FilterByItem = {
33
+ title: string;
34
+ value: string;
35
+ };
36
+ type FacetBy = {
37
+ type: 'select' | 'multiSelect' | 'range';
38
+ title: string;
39
+ fieldKey: string;
40
+ };
41
+ type OrderBy = {
42
+ title: string;
43
+ field: string;
44
+ direction: 'ASC' | 'DESC';
45
+ };
46
+ type PageSize = {
47
+ size: number;
48
+ /** When set, this size is the initial page size (first `default` wins). */
49
+ default?: boolean;
50
+ };
51
+ type Facets = {
52
+ [key: string]: {
53
+ [key: string]: number;
54
+ };
55
+ };
56
+ type FilterQuery = {
57
+ [K: string]: string[];
58
+ };
59
+ type SearchItemConfig = {
60
+ source: string;
61
+ type: string;
62
+ nodeId?: string;
63
+ urlTemplate?: string;
64
+ tokenMapping?: Record<string, string>;
65
+ };
66
+
67
+ interface SearchParams {
68
+ projectId?: string;
69
+ page?: number;
70
+ perPage?: number;
71
+ filters?: Record<string, unknown>;
72
+ baseFilterBy?: string;
73
+ facetBy?: string;
74
+ queryBy?: string;
75
+ search?: string;
76
+ orderBy?: string;
77
+ locale?: string;
78
+ maxFacetValues?: number;
79
+ /** Max typos tolerated per word: 0, 1, or 2. (Typesense `num_typos`.) */
80
+ maxTypos?: number;
81
+ /** Minimum word length before a single typo is allowed. (Typesense `min_len_1typo`.) */
82
+ minLengthFor1Typo?: number;
83
+ /** Minimum word length before two typos are allowed. (Typesense `min_len_2typo`.) */
84
+ minLengthFor2Typos?: number;
85
+ /** If a query returns fewer than this many results, retry with more typos. (Typesense `typo_tokens_threshold`.) */
86
+ typoFallbackThreshold?: number;
87
+ /** Rank exact matches above typo-corrected ones. (Typesense `prioritize_exact_match`.) */
88
+ prioritizeExactMatch?: boolean;
89
+ }
90
+ interface SearchClient {
91
+ performSearch(params: SearchParams): Promise<CollectionResult>;
92
+ }
93
+ declare function createSearchClient(config: {
94
+ apiUrl: string;
95
+ apiKey?: string;
96
+ projectId?: string;
97
+ }): SearchClient;
98
+
99
+ export { type CollectionResult as C, type FacetBy as F, type OrderBy as O, type PageSize as P, type SearchHit as S, type Facets as a, type FilterByItem as b, type FilterQuery as c, type Pagination as d, type SearchClient as e, type SearchCollection as f, type SearchItemConfig as g, type SearchParams as h, type SearchResponseHighlight as i, type SearchResponseHighlightObject as j, createSearchClient as k };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OrderBy } from './client-CtTcEJUy.mjs';
2
- export { C as CollectionResult, F as FacetBy, a as Facets, b as FilterByItem, c as FilterQuery, P as PageSize, d as Pagination, S as SearchClient, e as SearchCollection, f as SearchHit, g as SearchItemConfig, h as SearchParams, i as createSearchClient } from './client-CtTcEJUy.mjs';
1
+ import { O as OrderBy, S as SearchHit } from './client-DOew5kqb.mjs';
2
+ export { C as CollectionResult, F as FacetBy, a as Facets, b as FilterByItem, c as FilterQuery, P as PageSize, d as Pagination, e as SearchClient, f as SearchCollection, g as SearchItemConfig, h as SearchParams, i as SearchResponseHighlight, j as SearchResponseHighlightObject, k as createSearchClient } from './client-DOew5kqb.mjs';
3
3
 
4
4
  declare const UNIFORM_SEARCH_QUERY_KEY = "search";
5
5
  declare const UNIFORM_SEARCH_PAGE_KEY = "page";
@@ -11,6 +11,21 @@ declare const DEFAULT_PAGE_SIZE = 10;
11
11
  declare const buildOrderByQuery: (orderBy: OrderBy) => string;
12
12
  declare const getSearchParamsFromUrl: (urlString: string) => Record<string, string | string[]>;
13
13
 
14
+ type HighlightMatch = {
15
+ html?: string;
16
+ values?: unknown[];
17
+ };
18
+ /**
19
+ * Resolve the highlight (if any) for a field on a search hit.
20
+ *
21
+ * @param hit A hit returned by `performSearch`.
22
+ * @param fieldPath The field name (dot-path for nested fields, e.g. `author.name`).
23
+ * @param fieldValue The hit's raw value for the field (used to align array highlights).
24
+ * @returns `{ html }` for a highlighted snippet, `{ values }` for per-item array
25
+ * highlights, or `undefined` when the field wasn't matched.
26
+ */
27
+ declare const getHighlightMatch: (hit: SearchHit, fieldPath: string, fieldValue?: unknown) => HighlightMatch | undefined;
28
+
14
29
  declare function flattenBlockParams<T = Record<string, unknown>>(items: unknown, locale?: string): T[];
15
30
 
16
- export { DEFAULT_PAGE_SIZE, FIRST_PAGE, OrderBy, UNIFORM_SEARCH_ORDER_BY_KEY, UNIFORM_SEARCH_PAGE_KEY, UNIFORM_SEARCH_PAGE_SIZE_KEY, UNIFORM_SEARCH_QUERY_KEY, buildOrderByQuery, flattenBlockParams, getSearchParamsFromUrl };
31
+ export { DEFAULT_PAGE_SIZE, FIRST_PAGE, type HighlightMatch, OrderBy, SearchHit, UNIFORM_SEARCH_ORDER_BY_KEY, UNIFORM_SEARCH_PAGE_KEY, UNIFORM_SEARCH_PAGE_SIZE_KEY, UNIFORM_SEARCH_QUERY_KEY, buildOrderByQuery, flattenBlockParams, getHighlightMatch, getSearchParamsFromUrl };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OrderBy } from './client-CtTcEJUy.js';
2
- export { C as CollectionResult, F as FacetBy, a as Facets, b as FilterByItem, c as FilterQuery, P as PageSize, d as Pagination, S as SearchClient, e as SearchCollection, f as SearchHit, g as SearchItemConfig, h as SearchParams, i as createSearchClient } from './client-CtTcEJUy.js';
1
+ import { O as OrderBy, S as SearchHit } from './client-DOew5kqb.js';
2
+ export { C as CollectionResult, F as FacetBy, a as Facets, b as FilterByItem, c as FilterQuery, P as PageSize, d as Pagination, e as SearchClient, f as SearchCollection, g as SearchItemConfig, h as SearchParams, i as SearchResponseHighlight, j as SearchResponseHighlightObject, k as createSearchClient } from './client-DOew5kqb.js';
3
3
 
4
4
  declare const UNIFORM_SEARCH_QUERY_KEY = "search";
5
5
  declare const UNIFORM_SEARCH_PAGE_KEY = "page";
@@ -11,6 +11,21 @@ declare const DEFAULT_PAGE_SIZE = 10;
11
11
  declare const buildOrderByQuery: (orderBy: OrderBy) => string;
12
12
  declare const getSearchParamsFromUrl: (urlString: string) => Record<string, string | string[]>;
13
13
 
14
+ type HighlightMatch = {
15
+ html?: string;
16
+ values?: unknown[];
17
+ };
18
+ /**
19
+ * Resolve the highlight (if any) for a field on a search hit.
20
+ *
21
+ * @param hit A hit returned by `performSearch`.
22
+ * @param fieldPath The field name (dot-path for nested fields, e.g. `author.name`).
23
+ * @param fieldValue The hit's raw value for the field (used to align array highlights).
24
+ * @returns `{ html }` for a highlighted snippet, `{ values }` for per-item array
25
+ * highlights, or `undefined` when the field wasn't matched.
26
+ */
27
+ declare const getHighlightMatch: (hit: SearchHit, fieldPath: string, fieldValue?: unknown) => HighlightMatch | undefined;
28
+
14
29
  declare function flattenBlockParams<T = Record<string, unknown>>(items: unknown, locale?: string): T[];
15
30
 
16
- export { DEFAULT_PAGE_SIZE, FIRST_PAGE, OrderBy, UNIFORM_SEARCH_ORDER_BY_KEY, UNIFORM_SEARCH_PAGE_KEY, UNIFORM_SEARCH_PAGE_SIZE_KEY, UNIFORM_SEARCH_QUERY_KEY, buildOrderByQuery, flattenBlockParams, getSearchParamsFromUrl };
31
+ export { DEFAULT_PAGE_SIZE, FIRST_PAGE, type HighlightMatch, OrderBy, SearchHit, UNIFORM_SEARCH_ORDER_BY_KEY, UNIFORM_SEARCH_PAGE_KEY, UNIFORM_SEARCH_PAGE_SIZE_KEY, UNIFORM_SEARCH_QUERY_KEY, buildOrderByQuery, flattenBlockParams, getHighlightMatch, getSearchParamsFromUrl };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ __export(src_exports, {
29
29
  buildOrderByQuery: () => buildOrderByQuery,
30
30
  createSearchClient: () => createSearchClient,
31
31
  flattenBlockParams: () => flattenBlockParams,
32
+ getHighlightMatch: () => getHighlightMatch,
32
33
  getSearchParamsFromUrl: () => getSearchParamsFromUrl
33
34
  });
34
35
  module.exports = __toCommonJS(src_exports);
@@ -67,12 +68,12 @@ function createSearchClient(config) {
67
68
  body: JSON.stringify(body)
68
69
  });
69
70
  if (!response.ok) {
70
- console.error(`[alex] Search API error: ${response.status}`);
71
+ console.error(`Search API error: ${response.status}`);
71
72
  return emptyResult();
72
73
  }
73
74
  return response.json();
74
75
  } catch (error) {
75
- console.error("[alex] Search fetch error:", error);
76
+ console.error("Search fetch error:", error);
76
77
  return emptyResult();
77
78
  }
78
79
  }
@@ -106,6 +107,50 @@ var getSearchParamsFromUrl = (urlString) => {
106
107
  return params;
107
108
  };
108
109
 
110
+ // src/highlights.ts
111
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
112
+ var isHighlightLeaf = (value) => isPlainObject(value) && ("snippet" in value || "snippets" in value || "value" in value || "values" in value);
113
+ var unknownToString = (value) => {
114
+ if (value === null || value === void 0) return "";
115
+ if (typeof value === "string") return value;
116
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
117
+ return String(value);
118
+ }
119
+ try {
120
+ return JSON.stringify(value);
121
+ } catch (e) {
122
+ return "";
123
+ }
124
+ };
125
+ var getHighlightMatch = (hit, fieldPath, fieldValue) => {
126
+ const fieldHighlight = Array.isArray(hit._highlights) ? hit._highlights.find((highlight) => isPlainObject(highlight) && highlight.field === fieldPath) : void 0;
127
+ if (isPlainObject(fieldHighlight)) {
128
+ const snippets = fieldHighlight.snippets;
129
+ if (Array.isArray(snippets)) return { html: snippets.map(unknownToString).join(", ") };
130
+ const snippet = unknownToString(fieldHighlight.snippet);
131
+ if (snippet) return { html: snippet };
132
+ const value = unknownToString(fieldHighlight.value);
133
+ if (value) return { html: value };
134
+ }
135
+ const highlightValue = fieldPath.split(".").reduce((current, pathPart) => {
136
+ if (!isPlainObject(current)) return void 0;
137
+ return current[pathPart];
138
+ }, hit._highlight);
139
+ if (isHighlightLeaf(highlightValue)) {
140
+ if (Array.isArray(highlightValue.snippets)) {
141
+ return { html: highlightValue.snippets.map(unknownToString).join(", ") };
142
+ }
143
+ const snippet = unknownToString(highlightValue.snippet);
144
+ if (snippet) return { html: snippet };
145
+ if (Array.isArray(highlightValue.values)) return { values: highlightValue.values };
146
+ if ("value" in highlightValue) return { html: unknownToString(highlightValue.value) };
147
+ }
148
+ if (Array.isArray(fieldValue) && Array.isArray(highlightValue)) {
149
+ return { values: highlightValue };
150
+ }
151
+ return void 0;
152
+ };
153
+
109
154
  // src/flattenBlockParams.ts
110
155
  function resolveFieldValue(field, locale = "en-us") {
111
156
  var _a;
@@ -142,6 +187,7 @@ function flattenBlockParams(items, locale = "en-us") {
142
187
  buildOrderByQuery,
143
188
  createSearchClient,
144
189
  flattenBlockParams,
190
+ getHighlightMatch,
145
191
  getSearchParamsFromUrl
146
192
  });
147
193
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/client.ts","../src/utils.ts","../src/flattenBlockParams.ts"],"sourcesContent":["export * from './types';\nexport * from './constants';\nexport * from './client';\nexport * from './utils';\nexport * from './flattenBlockParams';\n","export const UNIFORM_SEARCH_QUERY_KEY = 'search';\nexport const UNIFORM_SEARCH_PAGE_KEY = 'page';\nexport const UNIFORM_SEARCH_PAGE_SIZE_KEY = 'pageSize';\nexport const UNIFORM_SEARCH_ORDER_BY_KEY = 'orderBy';\nexport const FIRST_PAGE = 0;\nexport const DEFAULT_PAGE_SIZE = 10;\n","import type { CollectionResult } from './types';\n\nexport interface SearchParams {\n projectId?: string;\n page?: number;\n perPage?: number;\n filters?: Record<string, unknown>;\n baseFilterBy?: string;\n facetBy?: string;\n queryBy?: string;\n search?: string;\n orderBy?: string;\n locale?: string;\n maxFacetValues?: number;\n}\n\nexport interface SearchClient {\n performSearch(params: SearchParams): Promise<CollectionResult>;\n}\n\nconst emptyResult = (): CollectionResult => ({\n data: { items: [], total: 0, page: 0, perPage: 10, totalPages: 0 },\n facets: {},\n});\n\nexport function createSearchClient(config: { apiUrl: string; apiKey?: string; projectId?: string }): SearchClient {\n const baseUrl = config.apiUrl.replace(/\\/+$/, '');\n return {\n async performSearch(params) {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey) {\n headers['x-api-key'] = config.apiKey;\n }\n\n const body = {\n ...params,\n projectId: params.projectId || config.projectId,\n };\n\n try {\n const response = await fetch(`${baseUrl}/api/search`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n console.error(`[alex] Search API error: ${response.status}`);\n return emptyResult();\n }\n\n return response.json();\n } catch (error) {\n console.error('[alex] Search fetch error:', error);\n return emptyResult();\n }\n },\n };\n}\n","import type { OrderBy } from './types';\n\nexport const buildOrderByQuery = (orderBy: OrderBy) => {\n const field = orderBy.field as unknown;\n // sortByConfig produces an object value: { field: string; direction: 'asc' | 'desc' }\n if (field && typeof field === 'object' && 'field' in (field as object)) {\n const sv = field as { field: string; direction: string };\n return `${sv.field}_${sv.direction.toUpperCase()}`;\n }\n // Plain string \"relevance\" or missing direction → no sort\n if (!field || field === 'relevance') return '';\n return `${field}_${orderBy.direction ?? 'ASC'}`;\n};\n\nexport const getSearchParamsFromUrl = (urlString: string): Record<string, string | string[]> => {\n if (!urlString) {\n return {};\n }\n\n const url = new URL(urlString);\n const params: Record<string, string | string[]> = {};\n\n url.searchParams.forEach((value, key) => {\n if (params[key]) {\n params[key] = Array.isArray(params[key])\n ? [...(params[key] as string[]), value]\n : [params[key] as string, value];\n } else {\n params[key] = value;\n }\n });\n\n return params;\n};\n","type RawField = {\n type?: string;\n value?: unknown;\n locales?: Record<string, unknown>;\n};\n\ntype RawBlockItem = {\n type?: string;\n fields?: Record<string, RawField>;\n};\n\nfunction resolveFieldValue(field: RawField, locale = 'en-us'): unknown {\n if (!field || typeof field !== 'object') return field;\n\n if (field.type === '$block' && Array.isArray(field.value)) {\n return flattenBlockParams(field.value as RawBlockItem[], locale);\n }\n\n if (field.locales && typeof field.locales === 'object') {\n return field.locales[locale] ?? field.value;\n }\n\n return field.value;\n}\n\nexport function flattenBlockParams<T = Record<string, unknown>>(\n items: unknown,\n locale = 'en-us'\n): T[] {\n if (!Array.isArray(items)) return [];\n return items.map(item => {\n const result: Record<string, unknown> = {};\n const blockItem = item as RawBlockItem;\n if (blockItem.fields) {\n for (const [key, field] of Object.entries(blockItem.fields)) {\n result[key] = resolveFieldValue(field, locale);\n }\n }\n return result as T;\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/client.ts","../src/utils.ts","../src/highlights.ts","../src/flattenBlockParams.ts"],"sourcesContent":["export * from './types';\nexport * from './constants';\nexport * from './client';\nexport * from './utils';\nexport * from './highlights';\nexport * from './flattenBlockParams';\n","export const UNIFORM_SEARCH_QUERY_KEY = 'search';\nexport const UNIFORM_SEARCH_PAGE_KEY = 'page';\nexport const UNIFORM_SEARCH_PAGE_SIZE_KEY = 'pageSize';\nexport const UNIFORM_SEARCH_ORDER_BY_KEY = 'orderBy';\nexport const FIRST_PAGE = 0;\nexport const DEFAULT_PAGE_SIZE = 10;\n","import type { CollectionResult } from './types';\n\nexport interface SearchParams {\n projectId?: string;\n page?: number;\n perPage?: number;\n filters?: Record<string, unknown>;\n baseFilterBy?: string;\n facetBy?: string;\n queryBy?: string;\n search?: string;\n orderBy?: string;\n locale?: string;\n maxFacetValues?: number;\n\n // ── Typo tolerance (all optional; omitted → the search engine's defaults apply) ──\n /** Max typos tolerated per word: 0, 1, or 2. (Typesense `num_typos`.) */\n maxTypos?: number;\n /** Minimum word length before a single typo is allowed. (Typesense `min_len_1typo`.) */\n minLengthFor1Typo?: number;\n /** Minimum word length before two typos are allowed. (Typesense `min_len_2typo`.) */\n minLengthFor2Typos?: number;\n /** If a query returns fewer than this many results, retry with more typos. (Typesense `typo_tokens_threshold`.) */\n typoFallbackThreshold?: number;\n /** Rank exact matches above typo-corrected ones. (Typesense `prioritize_exact_match`.) */\n prioritizeExactMatch?: boolean;\n}\n\nexport interface SearchClient {\n performSearch(params: SearchParams): Promise<CollectionResult>;\n}\n\nconst emptyResult = (): CollectionResult => ({\n data: { items: [], total: 0, page: 0, perPage: 10, totalPages: 0 },\n facets: {},\n});\n\nexport function createSearchClient(config: { apiUrl: string; apiKey?: string; projectId?: string }): SearchClient {\n const baseUrl = config.apiUrl.replace(/\\/+$/, '');\n return {\n async performSearch(params) {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey) {\n headers['x-api-key'] = config.apiKey;\n }\n\n const body = {\n ...params,\n projectId: params.projectId || config.projectId,\n };\n\n try {\n const response = await fetch(`${baseUrl}/api/search`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n console.error(`Search API error: ${response.status}`);\n return emptyResult();\n }\n\n return response.json();\n } catch (error) {\n console.error('Search fetch error:', error);\n return emptyResult();\n }\n },\n };\n}\n","import type { OrderBy } from './types';\n\nexport const buildOrderByQuery = (orderBy: OrderBy) => {\n const field = orderBy.field as unknown;\n // sortByConfig produces an object value: { field: string; direction: 'asc' | 'desc' }\n if (field && typeof field === 'object' && 'field' in (field as object)) {\n const sv = field as { field: string; direction: string };\n return `${sv.field}_${sv.direction.toUpperCase()}`;\n }\n // Plain string \"relevance\" or missing direction → no sort\n if (!field || field === 'relevance') return '';\n return `${field}_${orderBy.direction ?? 'ASC'}`;\n};\n\nexport const getSearchParamsFromUrl = (urlString: string): Record<string, string | string[]> => {\n if (!urlString) {\n return {};\n }\n\n const url = new URL(urlString);\n const params: Record<string, string | string[]> = {};\n\n url.searchParams.forEach((value, key) => {\n if (params[key]) {\n params[key] = Array.isArray(params[key])\n ? [...(params[key] as string[]), value]\n : [params[key] as string, value];\n } else {\n params[key] = value;\n }\n });\n\n return params;\n};\n","import type { SearchHit, SearchResponseHighlightObject } from './types';\n\n// Helpers for rendering search highlights returned by `performSearch`. A hit carries\n// highlight data in `_highlight` (nested by field) and/or `_highlights` (array form);\n// `getHighlightMatch` resolves the highlight for a given field into a simple shape the\n// UI can render — `html` (a snippet with the matched terms wrapped in highlight tags,\n// for `dangerouslySetInnerHTML`) or `values` (per-item highlights for array fields).\n\nexport type HighlightMatch = {\n html?: string;\n values?: unknown[];\n};\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isHighlightLeaf = (value: unknown): value is SearchResponseHighlightObject =>\n isPlainObject(value) &&\n ('snippet' in value || 'snippets' in value || 'value' in value || 'values' in value);\n\nconst unknownToString = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n};\n\n/**\n * Resolve the highlight (if any) for a field on a search hit.\n *\n * @param hit A hit returned by `performSearch`.\n * @param fieldPath The field name (dot-path for nested fields, e.g. `author.name`).\n * @param fieldValue The hit's raw value for the field (used to align array highlights).\n * @returns `{ html }` for a highlighted snippet, `{ values }` for per-item array\n * highlights, or `undefined` when the field wasn't matched.\n */\nexport const getHighlightMatch = (\n hit: SearchHit,\n fieldPath: string,\n fieldValue?: unknown\n): HighlightMatch | undefined => {\n const fieldHighlight = Array.isArray(hit._highlights)\n ? hit._highlights.find((highlight) => isPlainObject(highlight) && highlight.field === fieldPath)\n : undefined;\n\n if (isPlainObject(fieldHighlight)) {\n const snippets = fieldHighlight.snippets;\n if (Array.isArray(snippets)) return { html: snippets.map(unknownToString).join(', ') };\n const snippet = unknownToString(fieldHighlight.snippet);\n if (snippet) return { html: snippet };\n const value = unknownToString(fieldHighlight.value);\n if (value) return { html: value };\n }\n\n const highlightValue = fieldPath.split('.').reduce<unknown>((current, pathPart) => {\n if (!isPlainObject(current)) return undefined;\n return current[pathPart];\n }, hit._highlight);\n\n if (isHighlightLeaf(highlightValue)) {\n if (Array.isArray(highlightValue.snippets)) {\n return { html: highlightValue.snippets.map(unknownToString).join(', ') };\n }\n const snippet = unknownToString(highlightValue.snippet);\n if (snippet) return { html: snippet };\n if (Array.isArray(highlightValue.values)) return { values: highlightValue.values };\n if ('value' in highlightValue) return { html: unknownToString(highlightValue.value) };\n }\n\n if (Array.isArray(fieldValue) && Array.isArray(highlightValue)) {\n return { values: highlightValue };\n }\n\n return undefined;\n};\n","type RawField = {\n type?: string;\n value?: unknown;\n locales?: Record<string, unknown>;\n};\n\ntype RawBlockItem = {\n type?: string;\n fields?: Record<string, RawField>;\n};\n\nfunction resolveFieldValue(field: RawField, locale = 'en-us'): unknown {\n if (!field || typeof field !== 'object') return field;\n\n if (field.type === '$block' && Array.isArray(field.value)) {\n return flattenBlockParams(field.value as RawBlockItem[], locale);\n }\n\n if (field.locales && typeof field.locales === 'object') {\n return field.locales[locale] ?? field.value;\n }\n\n return field.value;\n}\n\nexport function flattenBlockParams<T = Record<string, unknown>>(\n items: unknown,\n locale = 'en-us'\n): T[] {\n if (!Array.isArray(items)) return [];\n return items.map(item => {\n const result: Record<string, unknown> = {};\n const blockItem = item as RawBlockItem;\n if (blockItem.fields) {\n for (const [key, field] of Object.entries(blockItem.fields)) {\n result[key] = resolveFieldValue(field, locale);\n }\n }\n return result as T;\n });\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -36,17 +36,61 @@ function createSearchClient(config) {
36
36
  body: JSON.stringify(body)
37
37
  });
38
38
  if (!response.ok) {
39
- console.error(`[alex] Search API error: ${response.status}`);
39
+ console.error(`Search API error: ${response.status}`);
40
40
  return emptyResult();
41
41
  }
42
42
  return response.json();
43
43
  } catch (error) {
44
- console.error("[alex] Search fetch error:", error);
44
+ console.error("Search fetch error:", error);
45
45
  return emptyResult();
46
46
  }
47
47
  }
48
48
  };
49
49
  }
50
+
51
+ // src/highlights.ts
52
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
53
+ var isHighlightLeaf = (value) => isPlainObject(value) && ("snippet" in value || "snippets" in value || "value" in value || "values" in value);
54
+ var unknownToString = (value) => {
55
+ if (value === null || value === void 0) return "";
56
+ if (typeof value === "string") return value;
57
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
58
+ return String(value);
59
+ }
60
+ try {
61
+ return JSON.stringify(value);
62
+ } catch (e) {
63
+ return "";
64
+ }
65
+ };
66
+ var getHighlightMatch = (hit, fieldPath, fieldValue) => {
67
+ const fieldHighlight = Array.isArray(hit._highlights) ? hit._highlights.find((highlight) => isPlainObject(highlight) && highlight.field === fieldPath) : void 0;
68
+ if (isPlainObject(fieldHighlight)) {
69
+ const snippets = fieldHighlight.snippets;
70
+ if (Array.isArray(snippets)) return { html: snippets.map(unknownToString).join(", ") };
71
+ const snippet = unknownToString(fieldHighlight.snippet);
72
+ if (snippet) return { html: snippet };
73
+ const value = unknownToString(fieldHighlight.value);
74
+ if (value) return { html: value };
75
+ }
76
+ const highlightValue = fieldPath.split(".").reduce((current, pathPart) => {
77
+ if (!isPlainObject(current)) return void 0;
78
+ return current[pathPart];
79
+ }, hit._highlight);
80
+ if (isHighlightLeaf(highlightValue)) {
81
+ if (Array.isArray(highlightValue.snippets)) {
82
+ return { html: highlightValue.snippets.map(unknownToString).join(", ") };
83
+ }
84
+ const snippet = unknownToString(highlightValue.snippet);
85
+ if (snippet) return { html: snippet };
86
+ if (Array.isArray(highlightValue.values)) return { values: highlightValue.values };
87
+ if ("value" in highlightValue) return { html: unknownToString(highlightValue.value) };
88
+ }
89
+ if (Array.isArray(fieldValue) && Array.isArray(highlightValue)) {
90
+ return { values: highlightValue };
91
+ }
92
+ return void 0;
93
+ };
50
94
  export {
51
95
  DEFAULT_PAGE_SIZE,
52
96
  FIRST_PAGE,
@@ -57,6 +101,7 @@ export {
57
101
  buildOrderByQuery,
58
102
  createSearchClient,
59
103
  flattenBlockParams,
104
+ getHighlightMatch,
60
105
  getSearchParamsFromUrl
61
106
  };
62
107
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type { CollectionResult } from './types';\n\nexport interface SearchParams {\n projectId?: string;\n page?: number;\n perPage?: number;\n filters?: Record<string, unknown>;\n baseFilterBy?: string;\n facetBy?: string;\n queryBy?: string;\n search?: string;\n orderBy?: string;\n locale?: string;\n maxFacetValues?: number;\n}\n\nexport interface SearchClient {\n performSearch(params: SearchParams): Promise<CollectionResult>;\n}\n\nconst emptyResult = (): CollectionResult => ({\n data: { items: [], total: 0, page: 0, perPage: 10, totalPages: 0 },\n facets: {},\n});\n\nexport function createSearchClient(config: { apiUrl: string; apiKey?: string; projectId?: string }): SearchClient {\n const baseUrl = config.apiUrl.replace(/\\/+$/, '');\n return {\n async performSearch(params) {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey) {\n headers['x-api-key'] = config.apiKey;\n }\n\n const body = {\n ...params,\n projectId: params.projectId || config.projectId,\n };\n\n try {\n const response = await fetch(`${baseUrl}/api/search`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n console.error(`[alex] Search API error: ${response.status}`);\n return emptyResult();\n }\n\n return response.json();\n } catch (error) {\n console.error('[alex] Search fetch error:', error);\n return emptyResult();\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,IAAM,cAAc,OAAyB;AAAA,EAC3C,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,IAAI,YAAY,EAAE;AAAA,EACjE,QAAQ,CAAC;AACX;AAEO,SAAS,mBAAmB,QAA+E;AAChH,QAAM,UAAU,OAAO,OAAO,QAAQ,QAAQ,EAAE;AAChD,SAAO;AAAA,IACL,MAAM,cAAc,QAAQ;AAC1B,YAAM,UAAkC;AAAA,QACtC,gBAAgB;AAAA,MAClB;AACA,UAAI,OAAO,QAAQ;AACjB,gBAAQ,WAAW,IAAI,OAAO;AAAA,MAChC;AAEA,YAAM,OAAO;AAAA,QACX,GAAG;AAAA,QACH,WAAW,OAAO,aAAa,OAAO;AAAA,MACxC;AAEA,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,eAAe;AAAA,UACpD,QAAQ;AAAA,UACR;AAAA,UACA,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AAED,YAAI,CAAC,SAAS,IAAI;AAChB,kBAAQ,MAAM,4BAA4B,SAAS,MAAM,EAAE;AAC3D,iBAAO,YAAY;AAAA,QACrB;AAEA,eAAO,SAAS,KAAK;AAAA,MACvB,SAAS,OAAO;AACd,gBAAQ,MAAM,8BAA8B,KAAK;AACjD,eAAO,YAAY;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/highlights.ts"],"sourcesContent":["import type { CollectionResult } from './types';\n\nexport interface SearchParams {\n projectId?: string;\n page?: number;\n perPage?: number;\n filters?: Record<string, unknown>;\n baseFilterBy?: string;\n facetBy?: string;\n queryBy?: string;\n search?: string;\n orderBy?: string;\n locale?: string;\n maxFacetValues?: number;\n\n // ── Typo tolerance (all optional; omitted → the search engine's defaults apply) ──\n /** Max typos tolerated per word: 0, 1, or 2. (Typesense `num_typos`.) */\n maxTypos?: number;\n /** Minimum word length before a single typo is allowed. (Typesense `min_len_1typo`.) */\n minLengthFor1Typo?: number;\n /** Minimum word length before two typos are allowed. (Typesense `min_len_2typo`.) */\n minLengthFor2Typos?: number;\n /** If a query returns fewer than this many results, retry with more typos. (Typesense `typo_tokens_threshold`.) */\n typoFallbackThreshold?: number;\n /** Rank exact matches above typo-corrected ones. (Typesense `prioritize_exact_match`.) */\n prioritizeExactMatch?: boolean;\n}\n\nexport interface SearchClient {\n performSearch(params: SearchParams): Promise<CollectionResult>;\n}\n\nconst emptyResult = (): CollectionResult => ({\n data: { items: [], total: 0, page: 0, perPage: 10, totalPages: 0 },\n facets: {},\n});\n\nexport function createSearchClient(config: { apiUrl: string; apiKey?: string; projectId?: string }): SearchClient {\n const baseUrl = config.apiUrl.replace(/\\/+$/, '');\n return {\n async performSearch(params) {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey) {\n headers['x-api-key'] = config.apiKey;\n }\n\n const body = {\n ...params,\n projectId: params.projectId || config.projectId,\n };\n\n try {\n const response = await fetch(`${baseUrl}/api/search`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n console.error(`Search API error: ${response.status}`);\n return emptyResult();\n }\n\n return response.json();\n } catch (error) {\n console.error('Search fetch error:', error);\n return emptyResult();\n }\n },\n };\n}\n","import type { SearchHit, SearchResponseHighlightObject } from './types';\n\n// Helpers for rendering search highlights returned by `performSearch`. A hit carries\n// highlight data in `_highlight` (nested by field) and/or `_highlights` (array form);\n// `getHighlightMatch` resolves the highlight for a given field into a simple shape the\n// UI can render — `html` (a snippet with the matched terms wrapped in highlight tags,\n// for `dangerouslySetInnerHTML`) or `values` (per-item highlights for array fields).\n\nexport type HighlightMatch = {\n html?: string;\n values?: unknown[];\n};\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isHighlightLeaf = (value: unknown): value is SearchResponseHighlightObject =>\n isPlainObject(value) &&\n ('snippet' in value || 'snippets' in value || 'value' in value || 'values' in value);\n\nconst unknownToString = (value: unknown): string => {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n};\n\n/**\n * Resolve the highlight (if any) for a field on a search hit.\n *\n * @param hit A hit returned by `performSearch`.\n * @param fieldPath The field name (dot-path for nested fields, e.g. `author.name`).\n * @param fieldValue The hit's raw value for the field (used to align array highlights).\n * @returns `{ html }` for a highlighted snippet, `{ values }` for per-item array\n * highlights, or `undefined` when the field wasn't matched.\n */\nexport const getHighlightMatch = (\n hit: SearchHit,\n fieldPath: string,\n fieldValue?: unknown\n): HighlightMatch | undefined => {\n const fieldHighlight = Array.isArray(hit._highlights)\n ? hit._highlights.find((highlight) => isPlainObject(highlight) && highlight.field === fieldPath)\n : undefined;\n\n if (isPlainObject(fieldHighlight)) {\n const snippets = fieldHighlight.snippets;\n if (Array.isArray(snippets)) return { html: snippets.map(unknownToString).join(', ') };\n const snippet = unknownToString(fieldHighlight.snippet);\n if (snippet) return { html: snippet };\n const value = unknownToString(fieldHighlight.value);\n if (value) return { html: value };\n }\n\n const highlightValue = fieldPath.split('.').reduce<unknown>((current, pathPart) => {\n if (!isPlainObject(current)) return undefined;\n return current[pathPart];\n }, hit._highlight);\n\n if (isHighlightLeaf(highlightValue)) {\n if (Array.isArray(highlightValue.snippets)) {\n return { html: highlightValue.snippets.map(unknownToString).join(', ') };\n }\n const snippet = unknownToString(highlightValue.snippet);\n if (snippet) return { html: snippet };\n if (Array.isArray(highlightValue.values)) return { values: highlightValue.values };\n if ('value' in highlightValue) return { html: unknownToString(highlightValue.value) };\n }\n\n if (Array.isArray(fieldValue) && Array.isArray(highlightValue)) {\n return { values: highlightValue };\n }\n\n return undefined;\n};\n"],"mappings":"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","names":[]}
package/dist/react.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { FC, ReactNode } from 'react';
3
- import { d as Pagination, f as SearchHit, a as Facets, P as PageSize, F as FacetBy, h as SearchParams, C as CollectionResult, g as SearchItemConfig } from './client-CtTcEJUy.mjs';
3
+ import { d as Pagination, S as SearchHit, a as Facets, P as PageSize, F as FacetBy, h as SearchParams, C as CollectionResult, g as SearchItemConfig } from './client-DOew5kqb.mjs';
4
4
 
5
5
  interface SearchProviderProps {
6
6
  children: ReactNode;
@@ -24,12 +24,21 @@ interface UseSearchReturn {
24
24
  pageSize: number;
25
25
  setPageSize: (size: number) => void;
26
26
  pageSizes: PageSize[];
27
+ /** Register a component's page-size config (last registration wins). `id` should be
28
+ * stable per instance. Used by pagination components that own their config. */
29
+ registerPageSizes: (id: string, pageSizes: unknown) => void;
30
+ unregisterPageSizes: (id: string) => void;
27
31
  orderByOptions: Array<{
28
32
  title: string;
29
33
  value: string;
30
34
  }>;
31
35
  selectedOrderBy: string;
32
36
  setOrderBy: (query: string) => void;
37
+ /** Register a sort component's order-by config (last registration wins). `id` should
38
+ * be stable per component instance. Used by sort components that own their config
39
+ * (App Router slots) instead of it being passed to the provider. */
40
+ registerOrderBy: (id: string, orderBy: unknown) => void;
41
+ unregisterOrderBy: (id: string) => void;
33
42
  filterOptions: FacetBy[];
34
43
  setFilterOptions: (filters: FacetBy[]) => void;
35
44
  /** Additive registration of a single facet option (keyed by fieldKey). Used by facet
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { FC, ReactNode } from 'react';
3
- import { d as Pagination, f as SearchHit, a as Facets, P as PageSize, F as FacetBy, h as SearchParams, C as CollectionResult, g as SearchItemConfig } from './client-CtTcEJUy.js';
3
+ import { d as Pagination, S as SearchHit, a as Facets, P as PageSize, F as FacetBy, h as SearchParams, C as CollectionResult, g as SearchItemConfig } from './client-DOew5kqb.js';
4
4
 
5
5
  interface SearchProviderProps {
6
6
  children: ReactNode;
@@ -24,12 +24,21 @@ interface UseSearchReturn {
24
24
  pageSize: number;
25
25
  setPageSize: (size: number) => void;
26
26
  pageSizes: PageSize[];
27
+ /** Register a component's page-size config (last registration wins). `id` should be
28
+ * stable per instance. Used by pagination components that own their config. */
29
+ registerPageSizes: (id: string, pageSizes: unknown) => void;
30
+ unregisterPageSizes: (id: string) => void;
27
31
  orderByOptions: Array<{
28
32
  title: string;
29
33
  value: string;
30
34
  }>;
31
35
  selectedOrderBy: string;
32
36
  setOrderBy: (query: string) => void;
37
+ /** Register a sort component's order-by config (last registration wins). `id` should
38
+ * be stable per component instance. Used by sort components that own their config
39
+ * (App Router slots) instead of it being passed to the provider. */
40
+ registerOrderBy: (id: string, orderBy: unknown) => void;
41
+ unregisterOrderBy: (id: string) => void;
33
42
  filterOptions: FacetBy[];
34
43
  setFilterOptions: (filters: FacetBy[]) => void;
35
44
  /** Additive registration of a single facet option (keyed by fieldKey). Used by facet