suparisma 1.1.2 → 1.2.2

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,430 @@
1
+ # 🔍 Suparisma Full-Text Search Documentation
2
+
3
+ ## Overview
4
+
5
+ Suparisma now includes powerful, type-safe full-text search capabilities powered by PostgreSQL's built-in full-text search engine. This implementation follows Supabase's full-text search patterns and provides both single-field and multi-field search functionality.
6
+
7
+ ## Features
8
+
9
+ ### ✅ Core Features
10
+ - **Type-Safe Search API**: Full TypeScript support with IntelliSense
11
+ - **PostgreSQL Full-Text Search**: Uses `to_tsvector` and `to_tsquery` for advanced text matching
12
+ - **Partial/Prefix Matching**: Search with `:*` operator for partial matches
13
+ - **GIN Indexes**: Automatically created for optimal performance
14
+ - **Multi-Field Search**: Search across multiple fields simultaneously
15
+ - **Real-time Results**: Search results update in real-time
16
+ - **Error Handling**: Robust error handling with fallback behavior
17
+ - **Debounced Queries**: 300ms debounce to prevent excessive API calls
18
+
19
+ ### 🎯 Search Types
20
+
21
+ #### 1. Single Field Search
22
+ ```typescript
23
+ // Search in a specific field
24
+ searchThings.searchField("name", "john");
25
+
26
+ // Or use the addQuery method
27
+ searchThings.addQuery({ field: "name", value: "john" });
28
+ ```
29
+
30
+ #### 2. Multi-Field Search
31
+ ```typescript
32
+ // Search across all searchable fields
33
+ searchThings.searchMultiField("john doe");
34
+
35
+ // Equivalent to:
36
+ searchThings.addQuery({ field: "multi", value: "john doe" });
37
+ ```
38
+
39
+ #### 3. Manual Query Management
40
+ ```typescript
41
+ // Set multiple queries at once
42
+ searchThings.setQueries([
43
+ { field: "name", value: "john" },
44
+ { field: "description", value: "developer" }
45
+ ]);
46
+
47
+ // Remove specific field search
48
+ searchThings.removeQuery("name");
49
+
50
+ // Clear all searches
51
+ searchThings.clearQueries();
52
+ ```
53
+
54
+ ## Setup Instructions
55
+
56
+ ### 1. Enable Search in Prisma Schema
57
+
58
+ Add the `// @enableSearch` comment above any field you want to make searchable:
59
+
60
+ ```prisma
61
+ model User {
62
+ id String @id @default(uuid())
63
+ // @enableSearch
64
+ name String
65
+ // @enableSearch
66
+ email String @unique
67
+ createdAt DateTime @default(now())
68
+ updatedAt DateTime @updatedAt
69
+ }
70
+
71
+ model Thing {
72
+ id String @id @default(uuid())
73
+ // @enableSearch
74
+ name String?
75
+ // @enableSearch
76
+ description String?
77
+ someNumber Int?
78
+ createdAt DateTime @default(now())
79
+ updatedAt DateTime @updatedAt
80
+ }
81
+ ```
82
+
83
+ ### 2. Generate Hooks
84
+
85
+ Run the generation command:
86
+
87
+ ```bash
88
+ npm run generate-hooks-dev
89
+ ```
90
+
91
+ This will automatically:
92
+ - ✅ Create individual RPC functions for each searchable field
93
+ - ✅ Create multi-field RPC functions when multiple fields are searchable
94
+ - ✅ Generate GIN indexes for optimal performance
95
+ - ✅ Update TypeScript types with search capabilities
96
+
97
+ ### 3. Database Functions Created
98
+
99
+ For each searchable field, the generator creates:
100
+
101
+ ```sql
102
+ -- Individual field search function
103
+ CREATE OR REPLACE FUNCTION "public"."search_thing_by_name_prefix"(search_prefix text)
104
+ RETURNS SETOF "public"."Thing" AS $$
105
+ BEGIN
106
+ -- Handle empty or null search terms
107
+ IF search_prefix IS NULL OR trim(search_prefix) = '' THEN
108
+ RETURN;
109
+ END IF;
110
+
111
+ -- Return query with proper error handling
112
+ RETURN QUERY
113
+ SELECT * FROM "public"."Thing"
114
+ WHERE
115
+ "name" IS NOT NULL
116
+ AND "name" != ''
117
+ AND to_tsvector('english', "name") @@ to_tsquery('english', search_prefix || ':*');
118
+ EXCEPTION
119
+ WHEN others THEN
120
+ -- Log error and return empty result set instead of failing
121
+ RAISE NOTICE 'Search function error: %', SQLERRM;
122
+ RETURN;
123
+ END;
124
+ $$ LANGUAGE plpgsql STABLE;
125
+
126
+ -- Multi-field search function (when multiple fields are searchable)
127
+ CREATE OR REPLACE FUNCTION "public"."search_thing_multi_field"(search_prefix text)
128
+ RETURNS SETOF "public"."Thing" AS $$
129
+ BEGIN
130
+ -- Handle empty or null search terms
131
+ IF search_prefix IS NULL OR trim(search_prefix) = '' THEN
132
+ RETURN;
133
+ END IF;
134
+
135
+ -- Return query searching across all searchable fields
136
+ RETURN QUERY
137
+ SELECT * FROM "public"."Thing"
138
+ WHERE
139
+ to_tsvector('english',
140
+ COALESCE("name", '') || ' ' || COALESCE("description", '')
141
+ ) @@ to_tsquery('english', search_prefix || ':*');
142
+ EXCEPTION
143
+ WHEN others THEN
144
+ -- Log error and return empty result set instead of failing
145
+ RAISE NOTICE 'Multi-field search function error: %', SQLERRM;
146
+ RETURN;
147
+ END;
148
+ $$ LANGUAGE plpgsql STABLE;
149
+ ```
150
+
151
+ ### 4. GIN Indexes Created
152
+
153
+ ```sql
154
+ -- Individual field indexes
155
+ CREATE INDEX "idx_gin_search_thing_name" ON "public"."Thing"
156
+ USING GIN (to_tsvector('english', "name"));
157
+
158
+ CREATE INDEX "idx_gin_search_thing_description" ON "public"."Thing"
159
+ USING GIN (to_tsvector('english', "description"));
160
+
161
+ -- Multi-field index
162
+ CREATE INDEX "idx_gin_search_thing_multi_field" ON "public"."Thing"
163
+ USING GIN (to_tsvector('english',
164
+ COALESCE("name", '') || ' ' || COALESCE("description", '')
165
+ ));
166
+ ```
167
+
168
+ ## Usage Examples
169
+
170
+ ### Basic Usage
171
+
172
+ ```typescript
173
+ import useSuparisma from './generated';
174
+
175
+ function SearchExample() {
176
+ const things = useSuparisma.thing();
177
+
178
+ const handleSearch = (searchTerm: string) => {
179
+ if (searchTerm.trim()) {
180
+ // Search in name field only
181
+ things.search.searchField("name", searchTerm);
182
+ } else {
183
+ things.search.clearQueries();
184
+ }
185
+ };
186
+
187
+ const handleMultiSearch = (searchTerm: string) => {
188
+ if (searchTerm.trim()) {
189
+ // Search across all searchable fields
190
+ things.search.searchMultiField(searchTerm);
191
+ } else {
192
+ things.search.clearQueries();
193
+ }
194
+ };
195
+
196
+ return (
197
+ <div>
198
+ <input
199
+ placeholder="Search name..."
200
+ onChange={(e) => handleSearch(e.target.value)}
201
+ />
202
+
203
+ <input
204
+ placeholder="Search all fields..."
205
+ onChange={(e) => handleMultiSearch(e.target.value)}
206
+ />
207
+
208
+ {things.search.loading && <div>Searching...</div>}
209
+
210
+ {things.data.map(thing => (
211
+ <div key={thing.id}>
212
+ <h3>{thing.name}</h3>
213
+ <p>{thing.description}</p>
214
+ </div>
215
+ ))}
216
+ </div>
217
+ );
218
+ }
219
+ ```
220
+
221
+ ### Advanced Search Management
222
+
223
+ ```typescript
224
+ function AdvancedSearchExample() {
225
+ const things = useSuparisma.thing();
226
+
227
+ // Check current search state
228
+ const isSearching = things.search.queries.length > 0;
229
+ const currentQueries = things.search.queries;
230
+
231
+ // Add multiple search criteria
232
+ const handleComplexSearch = () => {
233
+ things.search.setQueries([
234
+ { field: "name", value: "john" },
235
+ { field: "description", value: "developer" }
236
+ ]);
237
+ };
238
+
239
+ // Remove specific search
240
+ const removeNameSearch = () => {
241
+ things.search.removeQuery("name");
242
+ };
243
+
244
+ return (
245
+ <div>
246
+ {isSearching && (
247
+ <div>
248
+ <p>Active searches: {currentQueries.length}</p>
249
+ <button onClick={() => things.search.clearQueries()}>
250
+ Clear All
251
+ </button>
252
+ </div>
253
+ )}
254
+
255
+ <button onClick={handleComplexSearch}>
256
+ Search Multiple Fields
257
+ </button>
258
+
259
+ <button onClick={removeNameSearch}>
260
+ Remove Name Search
261
+ </button>
262
+ </div>
263
+ );
264
+ }
265
+ ```
266
+
267
+ ### Search with Filtering
268
+
269
+ ```typescript
270
+ function SearchWithFilters() {
271
+ const things = useSuparisma.thing({
272
+ // Combine search with other filters
273
+ where: {
274
+ someNumber: { gt: 50 } // Only show items with number > 50
275
+ },
276
+ orderBy: { createdAt: 'desc' }
277
+ });
278
+
279
+ // Search will be applied on top of the where filter
280
+ const handleSearch = (term: string) => {
281
+ things.search.searchMultiField(term);
282
+ };
283
+
284
+ return (
285
+ <div>
286
+ <input
287
+ placeholder="Search (filtered results)..."
288
+ onChange={(e) => handleSearch(e.target.value)}
289
+ />
290
+
291
+ <p>Showing {things.count} results</p>
292
+
293
+ {things.data.map(thing => (
294
+ <div key={thing.id}>
295
+ <h3>{thing.name}</h3>
296
+ <p>Number: {thing.someNumber}</p>
297
+ </div>
298
+ ))}
299
+ </div>
300
+ );
301
+ }
302
+ ```
303
+
304
+ ## Search State API
305
+
306
+ The search object provides the following interface:
307
+
308
+ ```typescript
309
+ interface SearchState {
310
+ /** Current active search queries */
311
+ queries: SearchQuery[];
312
+
313
+ /** Whether a search is currently in progress */
314
+ loading: boolean;
315
+
316
+ /** Replace all search queries with a new set */
317
+ setQueries: (queries: SearchQuery[]) => void;
318
+
319
+ /** Add a new search query (replaces existing query for same field) */
320
+ addQuery: (query: SearchQuery) => void;
321
+
322
+ /** Remove a search query by field name */
323
+ removeQuery: (field: string) => void;
324
+
325
+ /** Clear all search queries and return to normal data fetching */
326
+ clearQueries: () => void;
327
+
328
+ /** Search across multiple fields (convenience method) */
329
+ searchMultiField: (value: string) => void;
330
+
331
+ /** Search in a specific field (convenience method) */
332
+ searchField: (field: string, value: string) => void;
333
+ }
334
+ ```
335
+
336
+ ## Performance Considerations
337
+
338
+ ### 1. GIN Indexes
339
+ - Automatically created for each searchable field
340
+ - Provides fast full-text search performance
341
+ - Indexes are updated automatically when data changes
342
+
343
+ ### 2. Debouncing
344
+ - 300ms debounce prevents excessive API calls
345
+ - Search requests are automatically batched
346
+
347
+ ### 3. Client-Side Filtering
348
+ - Search results are further filtered client-side if additional `where` conditions are applied
349
+ - Maintains real-time updates and complex filter combinations
350
+
351
+ ### 4. Error Handling
352
+ - RPC functions include error handling to prevent crashes
353
+ - Failed searches return empty results instead of throwing errors
354
+ - Client shows partial results if some searches fail
355
+
356
+ ## Real-Time Integration
357
+
358
+ Search functionality integrates seamlessly with Suparisma's real-time features:
359
+
360
+ - **Real-time Updates**: Search results update automatically when underlying data changes
361
+ - **Live Filtering**: Real-time events are filtered to match current search criteria
362
+ - **Consistent State**: Search state is maintained during real-time updates
363
+
364
+ ## Migration from Simple Search
365
+
366
+ If you're upgrading from a simpler search implementation:
367
+
368
+ 1. **Add `// @enableSearch` comments** to your Prisma schema
369
+ 2. **Run the generator** to create RPC functions and indexes
370
+ 3. **Update your UI** to use the new search methods:
371
+
372
+ ```typescript
373
+ // Old approach (if you had custom search)
374
+ const handleSearch = (term) => {
375
+ // Custom search logic
376
+ };
377
+
378
+ // New approach
379
+ const handleSearch = (term) => {
380
+ things.search.searchMultiField(term);
381
+ };
382
+ ```
383
+
384
+ ## Troubleshooting
385
+
386
+ ### Common Issues
387
+
388
+ 1. **RPC Function Not Found**
389
+ - Ensure you've run the generator after adding `// @enableSearch`
390
+ - Check that the field exists in your database table
391
+
392
+ 2. **No Search Results**
393
+ - Verify GIN indexes were created successfully
394
+ - Check that search terms are not empty
395
+ - Ensure fields contain text data
396
+
397
+ 3. **TypeScript Errors**
398
+ - Re-run the generator to update type definitions
399
+ - Ensure you're importing from the correct generated file
400
+
401
+ ### Debug Logging
402
+
403
+ The search implementation includes extensive console logging:
404
+
405
+ ```typescript
406
+ // Enable in development to see search activity
407
+ console.log('🔍 Executing search: search_thing_by_name_prefix(search_prefix: "john")');
408
+ console.log('🔍 Search results for "name": 5 items');
409
+ console.log('🔍 Combined search results: 8 unique items');
410
+ ```
411
+
412
+ ## Best Practices
413
+
414
+ 1. **Use Multi-Field Search** for general search boxes
415
+ 2. **Use Field-Specific Search** for targeted filtering
416
+ 3. **Combine with where filters** for complex queries
417
+ 4. **Clear searches** when navigating away from search views
418
+ 5. **Show loading states** during search operations
419
+ 6. **Debounce user input** to prevent excessive API calls (handled automatically)
420
+
421
+ ---
422
+
423
+ ## Next Steps
424
+
425
+ The search functionality is now fully integrated with Suparisma. You can:
426
+
427
+ - ✅ Add more searchable fields by adding `// @enableSearch` comments
428
+ - ✅ Combine search with complex filtering and sorting
429
+ - ✅ Build sophisticated search UIs with real-time updates
430
+ - ✅ Scale to handle large datasets with PostgreSQL's full-text search performance
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HOOK_NAME_PREFIX = exports.UTILS_DIR = exports.HOOKS_DIR = exports.TYPES_DIR = exports.OUTPUT_DIR = exports.PRISMA_SCHEMA_PATH = void 0;
6
+ exports.PLATFORM = exports.HOOK_NAME_PREFIX = exports.UTILS_DIR = exports.HOOKS_DIR = exports.TYPES_DIR = exports.OUTPUT_DIR = exports.PRISMA_SCHEMA_PATH = void 0;
7
7
  // Configuration
8
8
  const path_1 = __importDefault(require("path"));
9
9
  // Use current working directory for all paths
@@ -14,3 +14,4 @@ exports.TYPES_DIR = `${exports.OUTPUT_DIR}/types`;
14
14
  exports.HOOKS_DIR = `${exports.OUTPUT_DIR}/hooks`;
15
15
  exports.UTILS_DIR = `${exports.OUTPUT_DIR}/utils`;
16
16
  exports.HOOK_NAME_PREFIX = 'useSuparisma';
17
+ exports.PLATFORM = process.env.SUPARISMA_PLATFORM || 'web';