suparisma 1.1.1 → 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 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
- > ⚠️ **MAINTENANCE NOTICE**: Search functionality is currently under maintenance and may not work as expected. We're working on improvements and will update the documentation once it's fully operational.
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
- For fields annotated with `// @enableSearch`, you can use full-text search:
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
- // Search things by name
631
- const { data: searchResults } = useSuparisma.thing({
632
- search: {
633
- query: "cool",
634
- fields: ["name"]
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
- description String? // @enableSearch - Can add to multiple fields
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
- | `@enableSearch` | Enables full-text search on this field | Field (after definition) |
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