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.
- package/README.md +558 -18
- package/SEARCH_FEATURES.md +430 -0
- package/dist/config.js +2 -1
- package/dist/generators/coreGenerator.js +189 -43
- package/dist/generators/supabaseClientGenerator.js +69 -7
- package/dist/generators/typeGenerator.js +4 -21
- package/dist/index.js +206 -15
- package/dist/parser.js +47 -37
- package/package.json +22 -3
- package/prisma/schema.prisma +6 -1
|
@@ -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';
|