suparisma 1.1.2 → 1.2.1
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 +439 -14
- package/SEARCH_FEATURES.md +430 -0
- package/dist/generators/coreGenerator.js +152 -42
- package/dist/generators/typeGenerator.js +4 -21
- package/dist/index.js +206 -15
- package/dist/parser.js +47 -37
- package/package.json +1 -1
- package/prisma/schema.prisma +6 -1
package/README.md
CHANGED
|
@@ -26,6 +26,13 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
|
|
|
26
26
|
- [Sorting Data](#sorting-data)
|
|
27
27
|
- [Pagination](#pagination)
|
|
28
28
|
- [Search Functionality](#search-functionality)
|
|
29
|
+
- [Enabling Search](#enabling-search)
|
|
30
|
+
- [Search Methods](#search-methods)
|
|
31
|
+
- [Basic Search Examples](#basic-search-examples)
|
|
32
|
+
- [JSON Field Search](#json-field-search)
|
|
33
|
+
- [Advanced Search Features](#advanced-search-features)
|
|
34
|
+
- [Real-World Search Examples](#real-world-search-examples)
|
|
35
|
+
- [Search Implementation Details](#search-implementation-details)
|
|
29
36
|
- [Schema Annotations](#schema-annotations)
|
|
30
37
|
- [Building UI Components](#building-ui-components)
|
|
31
38
|
- [Table with Filtering, Sorting, and Pagination](#table-with-filtering-sorting-and-pagination)
|
|
@@ -622,18 +629,430 @@ const { data, count } = useSuparisma.thing();
|
|
|
622
629
|
|
|
623
630
|
### Search Functionality
|
|
624
631
|
|
|
625
|
-
|
|
632
|
+
Suparisma provides powerful PostgreSQL full-text search capabilities with automatic RPC function generation and type-safe search methods. Search is enabled per field using annotations in your Prisma schema.
|
|
626
633
|
|
|
627
|
-
|
|
634
|
+
#### Enabling Search
|
|
635
|
+
|
|
636
|
+
Add search annotations to your Prisma schema fields:
|
|
637
|
+
|
|
638
|
+
```prisma
|
|
639
|
+
model Post {
|
|
640
|
+
id String @id @default(uuid())
|
|
641
|
+
// @enableSearch - applies to the next field (inline)
|
|
642
|
+
title String
|
|
643
|
+
// @enableSearch - applies to the next field (standalone)
|
|
644
|
+
content String?
|
|
645
|
+
tags String[]
|
|
646
|
+
|
|
647
|
+
/// @enableSearch - applies to the NEXT field that comes after this comment
|
|
648
|
+
metadata Json?
|
|
649
|
+
|
|
650
|
+
createdAt DateTime @default(now())
|
|
651
|
+
updatedAt DateTime @updatedAt
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Three ways to enable search:**
|
|
656
|
+
|
|
657
|
+
1. **Inline comment**: `title String // @enableSearch`
|
|
658
|
+
2. **Standalone comment**: Place `// @enableSearch` on a line above the field
|
|
659
|
+
3. **Directive comment**: Place `/// @enableSearch` on a line, applies to the next field definition
|
|
660
|
+
|
|
661
|
+
#### Search Methods
|
|
662
|
+
|
|
663
|
+
Every searchable model provides comprehensive search functionality:
|
|
628
664
|
|
|
629
665
|
```tsx
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
search:
|
|
633
|
-
|
|
634
|
-
|
|
666
|
+
const {
|
|
667
|
+
data: posts,
|
|
668
|
+
search: searchPosts,
|
|
669
|
+
loading,
|
|
670
|
+
error
|
|
671
|
+
} = useSuparisma.post();
|
|
672
|
+
|
|
673
|
+
// The search object provides:
|
|
674
|
+
// - queries: SearchQuery[] // Current active search queries
|
|
675
|
+
// - loading: boolean // Search loading state
|
|
676
|
+
// - setQueries: (queries) => void // Set multiple search queries
|
|
677
|
+
// - addQuery: (query) => void // Add a single search query
|
|
678
|
+
// - removeQuery: (field) => void // Remove search by field
|
|
679
|
+
// - clearQueries: () => void // Clear all searches
|
|
680
|
+
// - searchMultiField: (value) => void // Search across all searchable fields
|
|
681
|
+
// - searchField: (field, value) => void // Search specific field
|
|
682
|
+
// - getCurrentSearchTerms: () => string[] // Get terms for highlighting
|
|
683
|
+
// - escapeRegex: (text) => string // Safely escape special characters
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
#### Basic Search Examples
|
|
687
|
+
|
|
688
|
+
```tsx
|
|
689
|
+
import useSuparisma from '../generated';
|
|
690
|
+
|
|
691
|
+
function PostSearch() {
|
|
692
|
+
const { data: posts, search: searchPosts } = useSuparisma.post();
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div>
|
|
696
|
+
{/* Search in a specific field */}
|
|
697
|
+
<input
|
|
698
|
+
placeholder="Search titles..."
|
|
699
|
+
onChange={(e) => {
|
|
700
|
+
if (e.target.value.trim()) {
|
|
701
|
+
searchPosts.searchField("title", e.target.value);
|
|
702
|
+
} else {
|
|
703
|
+
searchPosts.clearQueries();
|
|
704
|
+
}
|
|
705
|
+
}}
|
|
706
|
+
/>
|
|
707
|
+
|
|
708
|
+
{/* Search across all searchable fields */}
|
|
709
|
+
<input
|
|
710
|
+
placeholder="Search everywhere..."
|
|
711
|
+
onChange={(e) => {
|
|
712
|
+
if (e.target.value.trim()) {
|
|
713
|
+
searchPosts.searchMultiField(e.target.value);
|
|
714
|
+
} else {
|
|
715
|
+
searchPosts.clearQueries();
|
|
716
|
+
}
|
|
717
|
+
}}
|
|
718
|
+
/>
|
|
719
|
+
|
|
720
|
+
{/* Display results */}
|
|
721
|
+
{posts?.map(post => (
|
|
722
|
+
<div key={post.id}>
|
|
723
|
+
<h3>{post.title}</h3>
|
|
724
|
+
<p>{post.content}</p>
|
|
725
|
+
</div>
|
|
726
|
+
))}
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
#### JSON Field Search
|
|
733
|
+
|
|
734
|
+
Suparisma supports full-text search on JSON fields when annotated with `/// @enableSearch`:
|
|
735
|
+
|
|
736
|
+
```prisma
|
|
737
|
+
model Document {
|
|
738
|
+
id String @id @default(uuid())
|
|
739
|
+
title String // @enableSearch
|
|
740
|
+
|
|
741
|
+
/// @enableSearch - Enable search on the next JSON field
|
|
742
|
+
metadata Json? // Will be searchable as text
|
|
743
|
+
|
|
744
|
+
/// @enableSearch
|
|
745
|
+
content Json? // Complex JSON structures are converted to searchable text
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
JSON fields are automatically converted to searchable text during indexing:
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
function DocumentSearch() {
|
|
753
|
+
const { data: documents, search } = useSuparisma.document();
|
|
754
|
+
|
|
755
|
+
return (
|
|
756
|
+
<div>
|
|
757
|
+
{/* Search within JSON metadata */}
|
|
758
|
+
<input
|
|
759
|
+
placeholder="Search metadata..."
|
|
760
|
+
onChange={(e) => {
|
|
761
|
+
if (e.target.value.trim()) {
|
|
762
|
+
search.searchField("metadata", e.target.value);
|
|
763
|
+
} else {
|
|
764
|
+
search.clearQueries();
|
|
765
|
+
}
|
|
766
|
+
}}
|
|
767
|
+
/>
|
|
768
|
+
|
|
769
|
+
{/* Search across all fields including JSON */}
|
|
770
|
+
<input
|
|
771
|
+
placeholder="Search everything (including JSON)..."
|
|
772
|
+
onChange={(e) => {
|
|
773
|
+
if (e.target.value.trim()) {
|
|
774
|
+
search.searchMultiField(e.target.value);
|
|
775
|
+
} else {
|
|
776
|
+
search.clearQueries();
|
|
777
|
+
}
|
|
778
|
+
}}
|
|
779
|
+
/>
|
|
780
|
+
|
|
781
|
+
{documents?.map(doc => (
|
|
782
|
+
<div key={doc.id}>
|
|
783
|
+
<h3>{doc.title}</h3>
|
|
784
|
+
<pre>{JSON.stringify(doc.metadata, null, 2)}</pre>
|
|
785
|
+
</div>
|
|
786
|
+
))}
|
|
787
|
+
</div>
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**JSON Search Examples:**
|
|
793
|
+
```tsx
|
|
794
|
+
// Search for documents with specific metadata values
|
|
795
|
+
search.searchField("metadata", "author"); // Finds JSON containing "author"
|
|
796
|
+
search.searchField("content", "typescript"); // Finds JSON containing "typescript"
|
|
797
|
+
|
|
798
|
+
// Multi-field search includes JSON fields
|
|
799
|
+
search.searchMultiField("react tutorial"); // Searches title, metadata, content
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
#### Advanced Search Features
|
|
803
|
+
|
|
804
|
+
**Multi-word Search with AND Logic**
|
|
805
|
+
```tsx
|
|
806
|
+
// Searching "react typescript" finds posts containing BOTH words
|
|
807
|
+
searchPosts.searchField("title", "react typescript");
|
|
808
|
+
// Internally converts to: "react & typescript" for PostgreSQL
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Search Highlighting**
|
|
812
|
+
```tsx
|
|
813
|
+
function PostList() {
|
|
814
|
+
const { data: posts, search } = useSuparisma.post();
|
|
815
|
+
|
|
816
|
+
// Get current search terms for highlighting
|
|
817
|
+
const searchTerms = search.getCurrentSearchTerms();
|
|
818
|
+
|
|
819
|
+
const highlightText = (text: string, searchTerm: string) => {
|
|
820
|
+
if (!searchTerm) return text;
|
|
821
|
+
|
|
822
|
+
// Use the built-in regex escaping
|
|
823
|
+
const escapedTerm = search.escapeRegex(searchTerm);
|
|
824
|
+
const parts = text.split(new RegExp(`(${escapedTerm})`, 'gi'));
|
|
825
|
+
|
|
826
|
+
return parts.map((part, index) =>
|
|
827
|
+
part.toLowerCase() === searchTerm.toLowerCase() ? (
|
|
828
|
+
<mark key={index} className="bg-yellow-200">{part}</mark>
|
|
829
|
+
) : part
|
|
830
|
+
);
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
return (
|
|
834
|
+
<div>
|
|
835
|
+
{posts?.map(post => (
|
|
836
|
+
<div key={post.id}>
|
|
837
|
+
<h3>
|
|
838
|
+
{searchTerms.length > 0
|
|
839
|
+
? highlightText(post.title, searchTerms[0])
|
|
840
|
+
: post.title
|
|
841
|
+
}
|
|
842
|
+
</h3>
|
|
843
|
+
</div>
|
|
844
|
+
))}
|
|
845
|
+
</div>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Multiple Search Queries**
|
|
851
|
+
```tsx
|
|
852
|
+
// Search multiple fields simultaneously
|
|
853
|
+
search.setQueries([
|
|
854
|
+
{ field: "title", value: "react" },
|
|
855
|
+
{ field: "content", value: "tutorial" }
|
|
856
|
+
]);
|
|
857
|
+
|
|
858
|
+
// Add individual searches
|
|
859
|
+
search.addQuery({ field: "title", value: "javascript" });
|
|
860
|
+
search.addQuery({ field: "tags", value: "frontend" });
|
|
861
|
+
|
|
862
|
+
// Remove specific search
|
|
863
|
+
search.removeQuery("title");
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Search State Management**
|
|
867
|
+
```tsx
|
|
868
|
+
function SearchComponent() {
|
|
869
|
+
const { search } = useSuparisma.post();
|
|
870
|
+
|
|
871
|
+
// Monitor search state
|
|
872
|
+
if (search.loading) {
|
|
873
|
+
return <div>Searching...</div>;
|
|
635
874
|
}
|
|
636
|
-
|
|
875
|
+
|
|
876
|
+
// Display active searches
|
|
877
|
+
if (search.queries.length > 0) {
|
|
878
|
+
return (
|
|
879
|
+
<div>
|
|
880
|
+
<p>Active searches:</p>
|
|
881
|
+
{search.queries.map((query, index) => (
|
|
882
|
+
<span key={index} className="tag">
|
|
883
|
+
{query.field}: "{query.value}"
|
|
884
|
+
<button onClick={() => search.removeQuery(query.field)}>
|
|
885
|
+
×
|
|
886
|
+
</button>
|
|
887
|
+
</span>
|
|
888
|
+
))}
|
|
889
|
+
<button onClick={search.clearQueries}>Clear All</button>
|
|
890
|
+
</div>
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return <div>No active searches</div>;
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
#### Real-World Search Examples
|
|
899
|
+
|
|
900
|
+
**E-commerce Product Search**
|
|
901
|
+
```tsx
|
|
902
|
+
function ProductSearch() {
|
|
903
|
+
const { data: products, search } = useSuparisma.product();
|
|
904
|
+
const [searchType, setSearchType] = useState<'name' | 'description' | 'multi'>('multi');
|
|
905
|
+
|
|
906
|
+
const handleSearch = (value: string) => {
|
|
907
|
+
if (!value.trim()) {
|
|
908
|
+
search.clearQueries();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
switch (searchType) {
|
|
913
|
+
case 'name':
|
|
914
|
+
search.searchField('name', value);
|
|
915
|
+
break;
|
|
916
|
+
case 'description':
|
|
917
|
+
search.searchField('description', value);
|
|
918
|
+
break;
|
|
919
|
+
case 'multi':
|
|
920
|
+
search.searchMultiField(value);
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
<div>
|
|
927
|
+
{/* Search type selector */}
|
|
928
|
+
<div className="search-controls">
|
|
929
|
+
<select value={searchType} onChange={(e) => setSearchType(e.target.value)}>
|
|
930
|
+
<option value="multi">Search All Fields</option>
|
|
931
|
+
<option value="name">Product Name Only</option>
|
|
932
|
+
<option value="description">Description Only</option>
|
|
933
|
+
</select>
|
|
934
|
+
|
|
935
|
+
<input
|
|
936
|
+
placeholder={`Search ${searchType === 'multi' ? 'products' : searchType}...`}
|
|
937
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
938
|
+
/>
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
{/* Results with highlighting */}
|
|
942
|
+
<div className="results">
|
|
943
|
+
{search.loading && <div>Searching...</div>}
|
|
944
|
+
{products?.map(product => (
|
|
945
|
+
<ProductCard key={product.id} product={product} />
|
|
946
|
+
))}
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
**Content Management Search**
|
|
954
|
+
```tsx
|
|
955
|
+
function ArticleSearch() {
|
|
956
|
+
const { data: articles, search } = useSuparisma.article();
|
|
957
|
+
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
|
958
|
+
|
|
959
|
+
const performSearch = (term: string) => {
|
|
960
|
+
if (term.trim()) {
|
|
961
|
+
search.searchMultiField(term);
|
|
962
|
+
// Add to history (avoid duplicates)
|
|
963
|
+
setSearchHistory(prev =>
|
|
964
|
+
[term, ...prev.filter(t => t !== term)].slice(0, 5)
|
|
965
|
+
);
|
|
966
|
+
} else {
|
|
967
|
+
search.clearQueries();
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
return (
|
|
972
|
+
<div>
|
|
973
|
+
<div className="search-box">
|
|
974
|
+
<input
|
|
975
|
+
placeholder="Search articles..."
|
|
976
|
+
onChange={(e) => performSearch(e.target.value)}
|
|
977
|
+
onKeyDown={(e) => {
|
|
978
|
+
if (e.key === 'Escape') {
|
|
979
|
+
e.currentTarget.value = '';
|
|
980
|
+
search.clearQueries();
|
|
981
|
+
}
|
|
982
|
+
}}
|
|
983
|
+
/>
|
|
984
|
+
|
|
985
|
+
{/* Search suggestions from history */}
|
|
986
|
+
{searchHistory.length > 0 && (
|
|
987
|
+
<div className="search-history">
|
|
988
|
+
<small>Recent searches:</small>
|
|
989
|
+
{searchHistory.map((term, index) => (
|
|
990
|
+
<button
|
|
991
|
+
key={index}
|
|
992
|
+
onClick={() => performSearch(term)}
|
|
993
|
+
className="suggestion"
|
|
994
|
+
>
|
|
995
|
+
{term}
|
|
996
|
+
</button>
|
|
997
|
+
))}
|
|
998
|
+
</div>
|
|
999
|
+
)}
|
|
1000
|
+
</div>
|
|
1001
|
+
|
|
1002
|
+
{/* Search stats */}
|
|
1003
|
+
{search.queries.length > 0 && (
|
|
1004
|
+
<div className="search-stats">
|
|
1005
|
+
Found {articles?.length || 0} articles for "{search.getCurrentSearchTerms().join(', ')}"
|
|
1006
|
+
{search.loading && <span> (searching...)</span>}
|
|
1007
|
+
</div>
|
|
1008
|
+
)}
|
|
1009
|
+
</div>
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
#### Search Implementation Details
|
|
1015
|
+
|
|
1016
|
+
**PostgreSQL Full-Text Search**
|
|
1017
|
+
- Uses `to_tsvector` and `to_tsquery` for efficient full-text search
|
|
1018
|
+
- Automatically creates GIN indexes for searchable fields (recommended)
|
|
1019
|
+
- Supports partial matching with prefix search (`:*`)
|
|
1020
|
+
- Multi-word queries use AND logic (`&`) for better precision
|
|
1021
|
+
|
|
1022
|
+
**Generated RPC Functions**
|
|
1023
|
+
Suparisma automatically generates PostgreSQL RPC functions for search:
|
|
1024
|
+
- `search_{model}_by_{field}_prefix` - Single field search
|
|
1025
|
+
- `search_{model}_multi_field` - Multi-field search
|
|
1026
|
+
|
|
1027
|
+
**Performance Considerations**
|
|
1028
|
+
- Search queries are debounced (300ms) to prevent excessive API calls
|
|
1029
|
+
- Results are cached and updated via realtime subscriptions
|
|
1030
|
+
- Large datasets benefit from database-level GIN indexes:
|
|
1031
|
+
|
|
1032
|
+
```sql
|
|
1033
|
+
-- Recommended indexes for better search performance
|
|
1034
|
+
CREATE INDEX IF NOT EXISTS idx_posts_title_gin
|
|
1035
|
+
ON posts USING gin(to_tsvector('english', title));
|
|
1036
|
+
|
|
1037
|
+
CREATE INDEX IF NOT EXISTS idx_posts_content_gin
|
|
1038
|
+
ON posts USING gin(to_tsvector('english', content));
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
**Error Handling**
|
|
1042
|
+
```tsx
|
|
1043
|
+
function SearchWithErrorHandling() {
|
|
1044
|
+
const { data, search, error } = useSuparisma.post();
|
|
1045
|
+
|
|
1046
|
+
useEffect(() => {
|
|
1047
|
+
if (error) {
|
|
1048
|
+
console.error('Search error:', error);
|
|
1049
|
+
// Fallback to basic filtering
|
|
1050
|
+
search.clearQueries();
|
|
1051
|
+
}
|
|
1052
|
+
}, [error]);
|
|
1053
|
+
|
|
1054
|
+
// Component implementation...
|
|
1055
|
+
}
|
|
637
1056
|
```
|
|
638
1057
|
|
|
639
1058
|
## Schema Annotations
|
|
@@ -652,18 +1071,24 @@ model AuditLog {
|
|
|
652
1071
|
|
|
653
1072
|
model Thing {
|
|
654
1073
|
id String @id @default(uuid())
|
|
655
|
-
name String? // @enableSearch - Enable full-text search for this field
|
|
656
|
-
|
|
1074
|
+
name String? // @enableSearch - Enable full-text search for this field (inline)
|
|
1075
|
+
// @enableSearch - Enable search for the field above (standalone)
|
|
1076
|
+
description String?
|
|
657
1077
|
someNumber Int
|
|
1078
|
+
|
|
1079
|
+
/// @enableSearch - Enable search for the NEXT field that comes after this comment
|
|
1080
|
+
metadata Json?
|
|
658
1081
|
}
|
|
659
1082
|
```
|
|
660
1083
|
|
|
661
1084
|
Available annotations:
|
|
662
1085
|
|
|
663
|
-
| Annotation | Description | Location |
|
|
664
|
-
|
|
665
|
-
| `@disableRealtime` | Disables real-time updates for this model | Model (before definition) |
|
|
666
|
-
|
|
|
1086
|
+
| Annotation | Description | Location | Example |
|
|
1087
|
+
|------------|-------------|----------|---------|
|
|
1088
|
+
| `@disableRealtime` | Disables real-time updates for this model | Model (before definition) | `// @disableRealtime`<br>`model AuditLog { ... }` |
|
|
1089
|
+
| `// @enableSearch` | Enables full-text search (inline) | Field (after definition) | `name String // @enableSearch` |
|
|
1090
|
+
| `// @enableSearch` | Enables full-text search (standalone) | Line above field | `// @enableSearch`<br>`name String` |
|
|
1091
|
+
| `/// @enableSearch` | Enables full-text search (directive) | Applies to next field | `/// @enableSearch`<br>`metadata Json?` |
|
|
667
1092
|
|
|
668
1093
|
## Building UI Components
|
|
669
1094
|
|