@rubixstudios/payload-typesense 1.0.0
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/LICENSE +22 -0
- package/README.md +182 -0
- package/dist/components/HeadlessSearchInput.d.ts +86 -0
- package/dist/components/HeadlessSearchInput.d.ts.map +1 -0
- package/dist/components/HeadlessSearchInput.js +602 -0
- package/dist/components/ThemeProvider.d.ts +10 -0
- package/dist/components/ThemeProvider.d.ts.map +1 -0
- package/dist/components/ThemeProvider.js +17 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/components/themes/hooks.d.ts +52 -0
- package/dist/components/themes/hooks.d.ts.map +1 -0
- package/dist/components/themes/hooks.js +177 -0
- package/dist/components/themes/index.d.ts +5 -0
- package/dist/components/themes/index.d.ts.map +1 -0
- package/dist/components/themes/index.js +4 -0
- package/dist/components/themes/themes.d.ts +6 -0
- package/dist/components/themes/themes.d.ts.map +1 -0
- package/dist/components/themes/themes.js +156 -0
- package/dist/components/themes/types.d.ts +147 -0
- package/dist/components/themes/types.d.ts.map +1 -0
- package/dist/components/themes/types.js +1 -0
- package/dist/components/themes/utils.d.ts +30 -0
- package/dist/components/themes/utils.d.ts.map +1 -0
- package/dist/components/themes/utils.js +397 -0
- package/dist/endpoints/customEndpointHandler.d.ts +3 -0
- package/dist/endpoints/customEndpointHandler.d.ts.map +1 -0
- package/dist/endpoints/customEndpointHandler.js +5 -0
- package/dist/endpoints/health.d.ts +12 -0
- package/dist/endpoints/health.d.ts.map +1 -0
- package/dist/endpoints/health.js +174 -0
- package/dist/endpoints/search.d.ts +13 -0
- package/dist/endpoints/search.d.ts.map +1 -0
- package/dist/endpoints/search.js +375 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/lib/cache.d.ts +41 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +96 -0
- package/dist/lib/config-validation.d.ts +75 -0
- package/dist/lib/config-validation.d.ts.map +1 -0
- package/dist/lib/config-validation.js +174 -0
- package/dist/lib/hooks.d.ts +4 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/hooks.js +54 -0
- package/dist/lib/initialization.d.ts +5 -0
- package/dist/lib/initialization.d.ts.map +1 -0
- package/dist/lib/initialization.js +102 -0
- package/dist/lib/schema-mapper.d.ts +14 -0
- package/dist/lib/schema-mapper.d.ts.map +1 -0
- package/dist/lib/schema-mapper.js +137 -0
- package/dist/lib/types.d.ts +183 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/typesense-client.d.ts +5 -0
- package/dist/lib/typesense-client.d.ts.map +1 -0
- package/dist/lib/typesense-client.js +20 -0
- package/package.json +92 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { searchCache } from '../lib/cache.js';
|
|
2
|
+
import { getValidationErrors, validateSearchParams } from '../lib/config-validation.js';
|
|
3
|
+
import { createDetailedHealthCheckHandler, createHealthCheckHandler } from './health.js';
|
|
4
|
+
// Universal search across all collections
|
|
5
|
+
const searchAllCollections = async (typesenseClient, pluginOptions, query, options)=>{
|
|
6
|
+
try {
|
|
7
|
+
// Universal search logic
|
|
8
|
+
// Check cache first
|
|
9
|
+
const cachedResult = searchCache.get(query, 'universal', options);
|
|
10
|
+
if (cachedResult) {
|
|
11
|
+
// Return cached result
|
|
12
|
+
return Response.json(cachedResult);
|
|
13
|
+
}
|
|
14
|
+
const enabledCollections = Object.entries(pluginOptions.collections || {}).filter(([_, config])=>config?.enabled);
|
|
15
|
+
// Process enabled collections
|
|
16
|
+
if (enabledCollections.length === 0) {
|
|
17
|
+
return Response.json({
|
|
18
|
+
error: 'No collections enabled for search'
|
|
19
|
+
}, {
|
|
20
|
+
status: 400
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Search all collections in parallel
|
|
24
|
+
const searchPromises = enabledCollections.map(async ([collectionName, config])=>{
|
|
25
|
+
try {
|
|
26
|
+
const searchParameters = {
|
|
27
|
+
highlight_full_fields: config?.searchFields?.join(',') || 'title,content',
|
|
28
|
+
num_typos: 0,
|
|
29
|
+
page: options.page,
|
|
30
|
+
per_page: Math.ceil(options.per_page / enabledCollections.length),
|
|
31
|
+
q: query,
|
|
32
|
+
query_by: config?.searchFields?.join(',') || 'title,content',
|
|
33
|
+
snippet_threshold: 30,
|
|
34
|
+
typo_tokens_threshold: 1
|
|
35
|
+
};
|
|
36
|
+
// Search collection
|
|
37
|
+
const results = await typesenseClient.collections(collectionName).documents().search(searchParameters);
|
|
38
|
+
// Process results
|
|
39
|
+
// Add collection metadata to each hit
|
|
40
|
+
return {
|
|
41
|
+
collection: collectionName,
|
|
42
|
+
displayName: config?.displayName || collectionName,
|
|
43
|
+
icon: config?.icon || '📄',
|
|
44
|
+
...results,
|
|
45
|
+
hits: results.hits?.map((hit)=>({
|
|
46
|
+
...hit,
|
|
47
|
+
collection: collectionName,
|
|
48
|
+
displayName: config?.displayName || collectionName,
|
|
49
|
+
icon: config?.icon || '📄'
|
|
50
|
+
})) || []
|
|
51
|
+
};
|
|
52
|
+
} catch (_error) {
|
|
53
|
+
// Handle search error
|
|
54
|
+
return {
|
|
55
|
+
collection: collectionName,
|
|
56
|
+
displayName: config?.displayName || collectionName,
|
|
57
|
+
error: _error instanceof Error ? _error.message : 'Unknown error',
|
|
58
|
+
found: 0,
|
|
59
|
+
hits: [],
|
|
60
|
+
icon: config?.icon || '📄'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const results = await Promise.all(searchPromises);
|
|
65
|
+
// Combine results
|
|
66
|
+
const combinedHits = results.flatMap((result)=>result.hits || []);
|
|
67
|
+
const totalFound = results.reduce((sum, result)=>sum + (result.found || 0), 0);
|
|
68
|
+
// Sort combined results by relevance (text_match score)
|
|
69
|
+
combinedHits.sort((a, b)=>(b.text_match || 0) - (a.text_match || 0));
|
|
70
|
+
const searchResult = {
|
|
71
|
+
collections: results.map((r)=>({
|
|
72
|
+
collection: r.collection,
|
|
73
|
+
displayName: r.displayName,
|
|
74
|
+
error: r.error,
|
|
75
|
+
found: r.found || 0,
|
|
76
|
+
icon: r.icon
|
|
77
|
+
})),
|
|
78
|
+
found: totalFound,
|
|
79
|
+
hits: combinedHits.slice(0, options.per_page),
|
|
80
|
+
page: options.page,
|
|
81
|
+
request_params: {
|
|
82
|
+
per_page: options.per_page,
|
|
83
|
+
q: query
|
|
84
|
+
},
|
|
85
|
+
search_cutoff: false,
|
|
86
|
+
search_time_ms: 0
|
|
87
|
+
};
|
|
88
|
+
// Cache the result
|
|
89
|
+
searchCache.set(query, searchResult, 'universal', options);
|
|
90
|
+
return Response.json(searchResult);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Handle universal search error
|
|
93
|
+
return Response.json({
|
|
94
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
95
|
+
error: 'Universal search failed'
|
|
96
|
+
}, {
|
|
97
|
+
status: 500
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
export const createSearchEndpoints = (typesenseClient, pluginOptions, lastSyncTime)=>{
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
handler: createCollectionsHandler(pluginOptions),
|
|
105
|
+
method: 'get',
|
|
106
|
+
path: '/search/collections'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
handler: createSuggestHandler(typesenseClient, pluginOptions),
|
|
110
|
+
method: 'get',
|
|
111
|
+
path: '/search/:collectionName/suggest'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
handler: createSearchHandler(typesenseClient, pluginOptions),
|
|
115
|
+
method: 'get',
|
|
116
|
+
path: '/search/:collectionName'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
handler: createAdvancedSearchHandler(typesenseClient, pluginOptions),
|
|
120
|
+
method: 'post',
|
|
121
|
+
path: '/search/:collectionName'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
handler: createSearchHandler(typesenseClient, pluginOptions),
|
|
125
|
+
method: 'get',
|
|
126
|
+
path: '/search'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
handler: createHealthCheckHandler(typesenseClient, pluginOptions, lastSyncTime),
|
|
130
|
+
method: 'get',
|
|
131
|
+
path: '/search/health'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
handler: createDetailedHealthCheckHandler(typesenseClient, pluginOptions, lastSyncTime),
|
|
135
|
+
method: 'get',
|
|
136
|
+
path: '/search/health/detailed'
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
};
|
|
140
|
+
const createSearchHandler = (typesenseClient, pluginOptions)=>{
|
|
141
|
+
return async (request)=>{
|
|
142
|
+
try {
|
|
143
|
+
// Extract query parameters from the request
|
|
144
|
+
const { params, query } = request;
|
|
145
|
+
// Extract collection name from URL path (fallback to params if available)
|
|
146
|
+
let collectionName;
|
|
147
|
+
let collectionNameStr;
|
|
148
|
+
if (request.url && typeof request.url === 'string') {
|
|
149
|
+
const url = new URL(request.url);
|
|
150
|
+
const pathParts = url.pathname.split('/');
|
|
151
|
+
const searchIndex = pathParts.indexOf('search');
|
|
152
|
+
if (searchIndex !== -1 && pathParts[searchIndex + 1]) {
|
|
153
|
+
collectionName = pathParts[searchIndex + 1] || '';
|
|
154
|
+
collectionNameStr = String(collectionName);
|
|
155
|
+
} else {
|
|
156
|
+
collectionName = '';
|
|
157
|
+
collectionNameStr = '';
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Fallback to params extraction
|
|
161
|
+
const { collectionName: paramCollectionName } = params || {};
|
|
162
|
+
collectionName = String(paramCollectionName || '');
|
|
163
|
+
collectionNameStr = collectionName;
|
|
164
|
+
}
|
|
165
|
+
// Extract search parameters
|
|
166
|
+
const q = typeof query?.q === 'string' ? query.q : '';
|
|
167
|
+
const pageParam = query?.page;
|
|
168
|
+
const perPageParam = query?.per_page;
|
|
169
|
+
const page = typeof pageParam === 'string' || typeof pageParam === 'number' ? parseInt(String(pageParam), 10) : 1;
|
|
170
|
+
const per_page = typeof perPageParam === 'string' || typeof perPageParam === 'number' ? parseInt(String(perPageParam), 10) : 10;
|
|
171
|
+
const sort_by = query?.sort_by;
|
|
172
|
+
// Validate parsed numbers
|
|
173
|
+
if (isNaN(page) || page < 1) {
|
|
174
|
+
return Response.json({
|
|
175
|
+
error: 'Invalid page parameter'
|
|
176
|
+
}, {
|
|
177
|
+
status: 400
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (isNaN(per_page) || per_page < 1 || per_page > 250) {
|
|
181
|
+
return Response.json({
|
|
182
|
+
error: 'Invalid per_page parameter'
|
|
183
|
+
}, {
|
|
184
|
+
status: 400
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Process search request
|
|
188
|
+
// Validate search parameters
|
|
189
|
+
const searchParams = {
|
|
190
|
+
page,
|
|
191
|
+
per_page,
|
|
192
|
+
q,
|
|
193
|
+
sort_by: sort_by
|
|
194
|
+
};
|
|
195
|
+
const validation = validateSearchParams(searchParams);
|
|
196
|
+
if (!validation.success) {
|
|
197
|
+
return Response.json({
|
|
198
|
+
details: getValidationErrors(validation.errors || []),
|
|
199
|
+
error: 'Invalid search parameters'
|
|
200
|
+
}, {
|
|
201
|
+
status: 400
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// If no collection specified, search across all enabled collections
|
|
205
|
+
if (!collectionName) {
|
|
206
|
+
if (!q || q.trim() === '') {
|
|
207
|
+
return Response.json({
|
|
208
|
+
details: 'Please provide a search query using ?q=your_search_term',
|
|
209
|
+
error: 'Query parameter "q" is required',
|
|
210
|
+
example: '/api/search?q=example'
|
|
211
|
+
}, {
|
|
212
|
+
status: 400
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const searchOptions = {
|
|
216
|
+
filters: {},
|
|
217
|
+
page,
|
|
218
|
+
per_page
|
|
219
|
+
};
|
|
220
|
+
if (sort_by && typeof sort_by === 'string') {
|
|
221
|
+
searchOptions.sort_by = sort_by;
|
|
222
|
+
}
|
|
223
|
+
return await searchAllCollections(typesenseClient, pluginOptions, q, searchOptions);
|
|
224
|
+
}
|
|
225
|
+
// Validate collection is enabled
|
|
226
|
+
if (!pluginOptions.collections?.[collectionNameStr]?.enabled) {
|
|
227
|
+
return Response.json({
|
|
228
|
+
error: 'Collection not enabled for search'
|
|
229
|
+
}, {
|
|
230
|
+
status: 400
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (!q) {
|
|
234
|
+
return Response.json({
|
|
235
|
+
error: 'Query parameter "q" is required'
|
|
236
|
+
}, {
|
|
237
|
+
status: 400
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const searchParameters = {
|
|
241
|
+
highlight_full_fields: pluginOptions.collections?.[collectionNameStr]?.searchFields?.join(',') || 'title,content',
|
|
242
|
+
num_typos: 0,
|
|
243
|
+
page: Number(page),
|
|
244
|
+
per_page: Number(per_page),
|
|
245
|
+
q: String(q),
|
|
246
|
+
query_by: pluginOptions.collections?.[collectionNameStr]?.searchFields?.join(',') || 'title,content',
|
|
247
|
+
snippet_threshold: 30,
|
|
248
|
+
typo_tokens_threshold: 1
|
|
249
|
+
};
|
|
250
|
+
// Add sorting
|
|
251
|
+
if (sort_by && typeof sort_by === 'string') {
|
|
252
|
+
searchParameters.sort_by = sort_by;
|
|
253
|
+
}
|
|
254
|
+
// Execute Typesense search
|
|
255
|
+
// Check cache first
|
|
256
|
+
const cacheOptions = {
|
|
257
|
+
collection: collectionName,
|
|
258
|
+
page,
|
|
259
|
+
per_page,
|
|
260
|
+
sort_by
|
|
261
|
+
};
|
|
262
|
+
const cachedResult = searchCache.get(q, collectionNameStr, cacheOptions);
|
|
263
|
+
if (cachedResult) {
|
|
264
|
+
// Return cached result
|
|
265
|
+
return Response.json(cachedResult);
|
|
266
|
+
}
|
|
267
|
+
const searchResults = await typesenseClient.collections(collectionNameStr).documents().search(searchParameters);
|
|
268
|
+
// Process search results
|
|
269
|
+
// Cache the result
|
|
270
|
+
searchCache.set(q, searchResults, collectionNameStr, cacheOptions);
|
|
271
|
+
return Response.json(searchResults);
|
|
272
|
+
} catch (_error) {
|
|
273
|
+
// Handle search error
|
|
274
|
+
return Response.json({
|
|
275
|
+
details: _error instanceof Error ? _error.message : 'Unknown error',
|
|
276
|
+
error: 'Search handler failed'
|
|
277
|
+
}, {
|
|
278
|
+
status: 500
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const createAdvancedSearchHandler = (typesenseClient, pluginOptions)=>{
|
|
284
|
+
return async (request)=>{
|
|
285
|
+
const { params, req } = request;
|
|
286
|
+
const { collectionName } = params || {};
|
|
287
|
+
const collectionNameStr = String(collectionName || '');
|
|
288
|
+
const body = await req?.json?.() || {};
|
|
289
|
+
if (!pluginOptions.collections?.[collectionNameStr]?.enabled) {
|
|
290
|
+
return Response.json({
|
|
291
|
+
error: 'Collection not enabled for search'
|
|
292
|
+
}, {
|
|
293
|
+
status: 400
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const searchResults = await typesenseClient.collections(collectionNameStr).documents().search(body);
|
|
298
|
+
return Response.json(searchResults);
|
|
299
|
+
} catch (_error) {
|
|
300
|
+
// Handle advanced search error
|
|
301
|
+
return Response.json({
|
|
302
|
+
error: 'Advanced search failed'
|
|
303
|
+
}, {
|
|
304
|
+
status: 500
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const createSuggestHandler = (typesenseClient, pluginOptions)=>{
|
|
310
|
+
return async (request)=>{
|
|
311
|
+
// Extract collection name from URL path
|
|
312
|
+
const url = new URL(request.url);
|
|
313
|
+
const pathParts = url.pathname.split('/');
|
|
314
|
+
const collectionName = pathParts[pathParts.indexOf('search') + 1];
|
|
315
|
+
const collectionNameStr = String(collectionName || '');
|
|
316
|
+
// Extract query parameters
|
|
317
|
+
const q = url.searchParams.get('q');
|
|
318
|
+
const limit = url.searchParams.get('limit') || '5';
|
|
319
|
+
if (!collectionName || !pluginOptions.collections?.[collectionNameStr]?.enabled) {
|
|
320
|
+
return Response.json({
|
|
321
|
+
error: 'Collection not enabled for search'
|
|
322
|
+
}, {
|
|
323
|
+
status: 400
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (!q) {
|
|
327
|
+
return Response.json({
|
|
328
|
+
error: 'Query parameter "q" is required'
|
|
329
|
+
}, {
|
|
330
|
+
status: 400
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const suggestResults = await typesenseClient.collections(collectionNameStr).documents().search({
|
|
335
|
+
highlight_full_fields: pluginOptions.collections?.[collectionNameStr]?.searchFields?.join(',') || 'title,content',
|
|
336
|
+
per_page: Number(limit),
|
|
337
|
+
q,
|
|
338
|
+
query_by: pluginOptions.collections?.[collectionNameStr]?.searchFields?.join(',') || 'title,content',
|
|
339
|
+
snippet_threshold: 30
|
|
340
|
+
});
|
|
341
|
+
return Response.json(suggestResults);
|
|
342
|
+
} catch (_error) {
|
|
343
|
+
// Handle suggest error
|
|
344
|
+
return Response.json({
|
|
345
|
+
error: 'Suggest failed'
|
|
346
|
+
}, {
|
|
347
|
+
status: 500
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
const createCollectionsHandler = (pluginOptions)=>{
|
|
353
|
+
return ()=>{
|
|
354
|
+
try {
|
|
355
|
+
const collections = Object.entries(pluginOptions.collections || {}).filter(([_, config])=>config?.enabled).map(([slug, config])=>({
|
|
356
|
+
slug,
|
|
357
|
+
displayName: config?.displayName || slug.charAt(0).toUpperCase() + slug.slice(1),
|
|
358
|
+
facetFields: config?.facetFields || [],
|
|
359
|
+
icon: config?.icon || '📄',
|
|
360
|
+
searchFields: config?.searchFields || []
|
|
361
|
+
}));
|
|
362
|
+
return Response.json({
|
|
363
|
+
categorized: pluginOptions.settings?.categorized || false,
|
|
364
|
+
collections
|
|
365
|
+
});
|
|
366
|
+
} catch (_error) {
|
|
367
|
+
// Handle collections error
|
|
368
|
+
return Response.json({
|
|
369
|
+
error: 'Failed to get collections'
|
|
370
|
+
}, {
|
|
371
|
+
status: 500
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Config } from 'payload';
|
|
2
|
+
export * from './components/index.js';
|
|
3
|
+
export type TypesenseSearchConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* Collections to index in Typesense
|
|
6
|
+
*/
|
|
7
|
+
collections?: Partial<Record<string, {
|
|
8
|
+
displayName?: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
facetFields?: string[];
|
|
11
|
+
icon?: string;
|
|
12
|
+
searchFields?: string[];
|
|
13
|
+
sortFields?: string[];
|
|
14
|
+
}>>;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Global plugin settings
|
|
18
|
+
*/
|
|
19
|
+
settings?: {
|
|
20
|
+
autoSync?: boolean;
|
|
21
|
+
batchSize?: number;
|
|
22
|
+
categorized?: boolean;
|
|
23
|
+
searchEndpoint?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Typesense server configuration
|
|
27
|
+
*/
|
|
28
|
+
typesense: {
|
|
29
|
+
apiKey: string;
|
|
30
|
+
connectionTimeoutSeconds?: number;
|
|
31
|
+
nodes: Array<{
|
|
32
|
+
host: string;
|
|
33
|
+
port: number | string;
|
|
34
|
+
protocol: 'http' | 'https';
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare const typesenseSearch: (pluginOptions: TypesenseSearchConfig) => (config: Config) => Config;
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAQrC,cAAc,uBAAuB,CAAA;AAErC,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CACnB,MAAM,CACJ,MAAM,EACN;QACE,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,OAAO,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;QACtB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;QACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;KACtB,CACF,CACF,CAAA;IAED,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,cAAc,CAAC,EAAE,MAAM,CAAA;KACxB,CAAA;IAED;;OAEG;IACH,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAA;QACd,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,KAAK,EAAE,KAAK,CAAC;YACX,IAAI,EAAE,MAAM,CAAA;YACZ,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;YACrB,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;SAC3B,CAAC,CAAA;KACH,CAAA;CACF,CAAA;AAED,eAAO,MAAM,eAAe,GACzB,eAAe,qBAAqB,MACpC,QAAQ,MAAM,KAAG,MA6DjB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createSearchEndpoints } from './endpoints/search.js';
|
|
2
|
+
import { initializeTypesenseCollections } from './lib/initialization.js';
|
|
3
|
+
import { mapPayloadDocumentToTypesense } from './lib/schema-mapper.js';
|
|
4
|
+
import { createTypesenseClient } from './lib/typesense-client.js';
|
|
5
|
+
export * from './components/index.js';
|
|
6
|
+
export const typesenseSearch = (pluginOptions)=>(config)=>{
|
|
7
|
+
if (pluginOptions.disabled) {
|
|
8
|
+
return config;
|
|
9
|
+
}
|
|
10
|
+
// Initialize Typesense client
|
|
11
|
+
const typesenseClient = createTypesenseClient(pluginOptions.typesense);
|
|
12
|
+
// Add search endpoints
|
|
13
|
+
config.endpoints = [
|
|
14
|
+
...config.endpoints || [],
|
|
15
|
+
...createSearchEndpoints(typesenseClient, pluginOptions, Date.now())
|
|
16
|
+
];
|
|
17
|
+
// Apply hooks to individual collections
|
|
18
|
+
if (pluginOptions.settings?.autoSync !== false && pluginOptions.collections) {
|
|
19
|
+
config.collections = (config.collections || []).map((collection)=>{
|
|
20
|
+
const collectionConfig = pluginOptions.collections?.[collection.slug];
|
|
21
|
+
if (collectionConfig?.enabled) {
|
|
22
|
+
return {
|
|
23
|
+
...collection,
|
|
24
|
+
hooks: {
|
|
25
|
+
...collection.hooks,
|
|
26
|
+
afterChange: [
|
|
27
|
+
...collection.hooks?.afterChange || [],
|
|
28
|
+
async ({ doc, operation, req: _req })=>{
|
|
29
|
+
await syncDocumentToTypesense(typesenseClient, collection.slug, doc, operation, collectionConfig);
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
afterDelete: [
|
|
33
|
+
...collection.hooks?.afterDelete || [],
|
|
34
|
+
async ({ doc, req: _req })=>{
|
|
35
|
+
await deleteDocumentFromTypesense(typesenseClient, collection.slug, doc.id);
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return collection;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Initialize collections in Typesense
|
|
45
|
+
const incomingOnInit = config.onInit;
|
|
46
|
+
config.onInit = async (payload)=>{
|
|
47
|
+
if (incomingOnInit) {
|
|
48
|
+
await incomingOnInit(payload);
|
|
49
|
+
}
|
|
50
|
+
await initializeTypesenseCollections(payload, typesenseClient, pluginOptions);
|
|
51
|
+
};
|
|
52
|
+
return config;
|
|
53
|
+
};
|
|
54
|
+
// Helper function to create collection if it doesn't exist
|
|
55
|
+
const createCollectionIfNotExists = async (typesenseClient, collectionSlug, config)=>{
|
|
56
|
+
const searchableFields = config?.searchFields || [
|
|
57
|
+
'title',
|
|
58
|
+
'content',
|
|
59
|
+
'description'
|
|
60
|
+
];
|
|
61
|
+
const facetFields = config?.facetFields || [];
|
|
62
|
+
// Base fields that every collection should have
|
|
63
|
+
const baseFields = [
|
|
64
|
+
{
|
|
65
|
+
name: 'id',
|
|
66
|
+
type: 'string'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'createdAt',
|
|
70
|
+
type: 'int64'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'updatedAt',
|
|
74
|
+
type: 'int64'
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
// Map searchable fields
|
|
78
|
+
const searchFields = searchableFields.map((field)=>({
|
|
79
|
+
name: field,
|
|
80
|
+
type: 'string',
|
|
81
|
+
facet: facetFields.includes(field)
|
|
82
|
+
}));
|
|
83
|
+
// Map facet-only fields (not in searchable fields)
|
|
84
|
+
const facetOnlyFields = facetFields.filter((field)=>!searchableFields.includes(field)).map((field)=>({
|
|
85
|
+
name: field,
|
|
86
|
+
type: 'string',
|
|
87
|
+
facet: true
|
|
88
|
+
}));
|
|
89
|
+
const schema = {
|
|
90
|
+
name: collectionSlug,
|
|
91
|
+
fields: [
|
|
92
|
+
...baseFields,
|
|
93
|
+
...searchFields,
|
|
94
|
+
...facetOnlyFields
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
await typesenseClient.collections().create(schema);
|
|
98
|
+
// Collection created successfully
|
|
99
|
+
};
|
|
100
|
+
// Sync functions for hooks
|
|
101
|
+
const syncDocumentToTypesense = async (typesenseClient, collectionSlug, doc, operation, config)=>{
|
|
102
|
+
try {
|
|
103
|
+
// First check if the collection exists, create it if it doesn't
|
|
104
|
+
try {
|
|
105
|
+
await typesenseClient.collections(collectionSlug).retrieve();
|
|
106
|
+
} catch (collectionError) {
|
|
107
|
+
if (collectionError.httpStatus === 404) {
|
|
108
|
+
// Collection not found, creating it
|
|
109
|
+
await createCollectionIfNotExists(typesenseClient, collectionSlug, config);
|
|
110
|
+
} else {
|
|
111
|
+
throw collectionError;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const typesenseDoc = mapPayloadDocumentToTypesense(doc, collectionSlug, config);
|
|
115
|
+
await typesenseClient.collections(collectionSlug).documents().upsert(typesenseDoc);
|
|
116
|
+
// Document synced successfully
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Handle document sync error
|
|
119
|
+
// Log the problematic document for debugging
|
|
120
|
+
if (error.message.includes('validation')) {
|
|
121
|
+
// Log problematic document details
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const deleteDocumentFromTypesense = async (typesenseClient, collectionSlug, docId)=>{
|
|
126
|
+
try {
|
|
127
|
+
// First check if the collection exists
|
|
128
|
+
try {
|
|
129
|
+
await typesenseClient.collections(collectionSlug).retrieve();
|
|
130
|
+
} catch (collectionError) {
|
|
131
|
+
if (collectionError.httpStatus === 404) {
|
|
132
|
+
// Collection not found, skipping delete
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw collectionError;
|
|
136
|
+
}
|
|
137
|
+
// Try to delete the document
|
|
138
|
+
await typesenseClient.collections(collectionSlug).documents(docId).delete();
|
|
139
|
+
// Document deleted successfully
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Handle specific error cases
|
|
142
|
+
if (error.httpStatus === 404) {
|
|
143
|
+
// Document not found, already deleted
|
|
144
|
+
} else {
|
|
145
|
+
// Handle document deletion error
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CacheOptions } from './types.js';
|
|
2
|
+
export declare class SearchCache<T = any> {
|
|
3
|
+
private cache;
|
|
4
|
+
private readonly defaultTTL;
|
|
5
|
+
private readonly maxSize;
|
|
6
|
+
constructor(options?: CacheOptions);
|
|
7
|
+
/**
|
|
8
|
+
* Generate cache key from search parameters
|
|
9
|
+
*/
|
|
10
|
+
private generateKey;
|
|
11
|
+
/**
|
|
12
|
+
* Clear expired entries
|
|
13
|
+
*/
|
|
14
|
+
cleanup(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Clear cache entries matching pattern
|
|
17
|
+
*/
|
|
18
|
+
clear(pattern?: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Get cached search result
|
|
21
|
+
*/
|
|
22
|
+
get(query: string, collection?: string, params?: Record<string, any>): null | T;
|
|
23
|
+
/**
|
|
24
|
+
* Get cache statistics
|
|
25
|
+
*/
|
|
26
|
+
getStats(): {
|
|
27
|
+
hitRate?: number;
|
|
28
|
+
maxSize: number;
|
|
29
|
+
size: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Check if cache has valid entry
|
|
33
|
+
*/
|
|
34
|
+
has(query: string, collection?: string, params?: Record<string, any>): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Set cached search result
|
|
37
|
+
*/
|
|
38
|
+
set(query: string, data: T, collection?: string, params?: Record<string, any>, ttl?: number): void;
|
|
39
|
+
}
|
|
40
|
+
export declare const searchCache: SearchCache<any>;
|
|
41
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/lib/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,YAAY,CAAA;AAE1D,qBAAa,WAAW,CAAC,CAAC,GAAG,GAAG;IAC9B,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;gBAEpB,OAAO,GAAE,YAAiB;IAKtC;;OAEG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,OAAO,IAAI,IAAI;IASf;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAa7B;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;IAiB/E;;OAEG;IACH,QAAQ,IAAI;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAO/D;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO;IAI9E;;OAEG;IACH,GAAG,CACD,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EACP,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,GAAG,CAAC,EAAE,MAAM,GACX,IAAI;CAiBR;AAGD,eAAO,MAAM,WAAW,kBAGtB,CAAA"}
|