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
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
|
|
|
16
16
|
- [Why Suparisma?](#why-suparisma)
|
|
17
17
|
- [Features](#features)
|
|
18
18
|
- [Installation](#installation)
|
|
19
|
+
- [React Native / Expo Setup](#react-native--expo-setup)
|
|
19
20
|
- [Quick Start](#quick-start)
|
|
20
21
|
- [Detailed Usage](#detailed-usage)
|
|
21
22
|
- [Basic CRUD Operations](#basic-crud-operations)
|
|
@@ -26,6 +27,13 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
|
|
|
26
27
|
- [Sorting Data](#sorting-data)
|
|
27
28
|
- [Pagination](#pagination)
|
|
28
29
|
- [Search Functionality](#search-functionality)
|
|
30
|
+
- [Enabling Search](#enabling-search)
|
|
31
|
+
- [Search Methods](#search-methods)
|
|
32
|
+
- [Basic Search Examples](#basic-search-examples)
|
|
33
|
+
- [JSON Field Search](#json-field-search)
|
|
34
|
+
- [Advanced Search Features](#advanced-search-features)
|
|
35
|
+
- [Real-World Search Examples](#real-world-search-examples)
|
|
36
|
+
- [Search Implementation Details](#search-implementation-details)
|
|
29
37
|
- [Schema Annotations](#schema-annotations)
|
|
30
38
|
- [Building UI Components](#building-ui-components)
|
|
31
39
|
- [Table with Filtering, Sorting, and Pagination](#table-with-filtering-sorting-and-pagination)
|
|
@@ -53,7 +61,7 @@ Suparisma bridges this gap by:
|
|
|
53
61
|
- Enabling easy **pagination, filtering, and search** on your data
|
|
54
62
|
- Leveraging both **Prisma** and **Supabase** official SDKs
|
|
55
63
|
- Respecting **Supabase's auth rules** for secure database access
|
|
56
|
-
- Working seamlessly with any React environment (Next.js, Remix, Tanstack Router, etc.)
|
|
64
|
+
- Working seamlessly with any React environment (Next.js, Remix, Tanstack Router, React Native, etc.)
|
|
57
65
|
|
|
58
66
|
## Features
|
|
59
67
|
|
|
@@ -63,7 +71,7 @@ Suparisma bridges this gap by:
|
|
|
63
71
|
- 🔍 **Full-text search** with configurable annotations *(currently under maintenance)*
|
|
64
72
|
- 🔢 **Pagination and sorting** built into every hook
|
|
65
73
|
- 🧩 **Prisma-like API** that feels familiar if you already use Prisma
|
|
66
|
-
- 📱 **Works with any React framework** including Next.js, Remix,
|
|
74
|
+
- 📱 **Works with any React framework** including Next.js, Remix, React Native, and Expo
|
|
67
75
|
- 🛠️ **Simple CLI** to generate hooks with a single command
|
|
68
76
|
|
|
69
77
|
## Installation
|
|
@@ -79,6 +87,110 @@ yarn add suparisma
|
|
|
79
87
|
pnpm install suparisma
|
|
80
88
|
```
|
|
81
89
|
|
|
90
|
+
## React Native / Expo Setup
|
|
91
|
+
|
|
92
|
+
Suparisma fully supports React Native and Expo projects. Follow these additional steps for mobile development:
|
|
93
|
+
|
|
94
|
+
### 1. Install Dependencies
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Install Suparisma and required dependencies
|
|
98
|
+
pnpm install suparisma @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
|
|
99
|
+
|
|
100
|
+
# For UUID generation support (recommended)
|
|
101
|
+
pnpm install react-native-get-random-values
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Add Polyfills
|
|
105
|
+
|
|
106
|
+
Add these imports at the very top of your app's entry point (e.g., `App.tsx` or `index.js`):
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// App.tsx or index.js - Add these at the VERY TOP before any other imports
|
|
110
|
+
import 'react-native-get-random-values'; // Required for UUID generation
|
|
111
|
+
import 'react-native-url-polyfill/auto'; // Required for Supabase
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Set Environment Variables
|
|
115
|
+
|
|
116
|
+
For **Expo** projects, use the `EXPO_PUBLIC_` prefix in your `.env` file:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
EXPO_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
120
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For **bare React Native** projects, use `react-native-dotenv` or similar.
|
|
124
|
+
|
|
125
|
+
### 4. Generate Hooks for React Native
|
|
126
|
+
|
|
127
|
+
Set the `SUPARISMA_PLATFORM` environment variable when generating:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Generate hooks for React Native / Expo
|
|
131
|
+
SUPARISMA_PLATFORM=react-native npx suparisma generate
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or add it to your `package.json` scripts:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"scripts": {
|
|
139
|
+
"suparisma:generate": "SUPARISMA_PLATFORM=react-native npx suparisma generate"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 5. Use the Hooks
|
|
145
|
+
|
|
146
|
+
The hooks work exactly the same as in web projects:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import React from 'react';
|
|
150
|
+
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
|
151
|
+
import useSuparisma from './src/suparisma/generated';
|
|
152
|
+
|
|
153
|
+
function ThingList() {
|
|
154
|
+
const {
|
|
155
|
+
data: things,
|
|
156
|
+
loading,
|
|
157
|
+
error,
|
|
158
|
+
create: createThing
|
|
159
|
+
} = useSuparisma.thing();
|
|
160
|
+
|
|
161
|
+
if (loading) return <Text>Loading...</Text>;
|
|
162
|
+
if (error) return <Text>Error: {error.message}</Text>;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<View>
|
|
166
|
+
<FlatList
|
|
167
|
+
data={things}
|
|
168
|
+
keyExtractor={(item) => item.id}
|
|
169
|
+
renderItem={({ item }) => (
|
|
170
|
+
<Text>{item.name} (Number: {item.someNumber})</Text>
|
|
171
|
+
)}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
<TouchableOpacity
|
|
175
|
+
onPress={() => createThing({
|
|
176
|
+
name: "New Thing",
|
|
177
|
+
someNumber: Math.floor(Math.random() * 100)
|
|
178
|
+
})}
|
|
179
|
+
>
|
|
180
|
+
<Text>Add Thing</Text>
|
|
181
|
+
</TouchableOpacity>
|
|
182
|
+
</View>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Platform Detection
|
|
188
|
+
|
|
189
|
+
The generated Supabase client automatically configures itself for React Native with:
|
|
190
|
+
- **AsyncStorage** for auth persistence
|
|
191
|
+
- **Session detection** disabled (not applicable in mobile)
|
|
192
|
+
- **Auto refresh token** enabled
|
|
193
|
+
|
|
82
194
|
## Quick Start
|
|
83
195
|
|
|
84
196
|
1. **Add a Prisma schema**: Ensure you have a valid `prisma/schema.prisma` file in your project
|
|
@@ -132,6 +244,7 @@ Note: you can adjust the prisma schema path and the generated files output path
|
|
|
132
244
|
```bash
|
|
133
245
|
SUPARISMA_PRISMA_SCHEMA_PATH="./prisma/schema.prisma"
|
|
134
246
|
SUPARISMA_OUTPUT_DIR="./src/suparisma/generated"
|
|
247
|
+
SUPARISMA_PLATFORM="web" # or "react-native" for React Native/Expo projects
|
|
135
248
|
```
|
|
136
249
|
Also make sure to not change any of these generated files directly as **they will always be overwritten**
|
|
137
250
|
|
|
@@ -622,18 +735,430 @@ const { data, count } = useSuparisma.thing();
|
|
|
622
735
|
|
|
623
736
|
### Search Functionality
|
|
624
737
|
|
|
625
|
-
|
|
738
|
+
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.
|
|
739
|
+
|
|
740
|
+
#### Enabling Search
|
|
741
|
+
|
|
742
|
+
Add search annotations to your Prisma schema fields:
|
|
743
|
+
|
|
744
|
+
```prisma
|
|
745
|
+
model Post {
|
|
746
|
+
id String @id @default(uuid())
|
|
747
|
+
// @enableSearch - applies to the next field (inline)
|
|
748
|
+
title String
|
|
749
|
+
// @enableSearch - applies to the next field (standalone)
|
|
750
|
+
content String?
|
|
751
|
+
tags String[]
|
|
752
|
+
|
|
753
|
+
/// @enableSearch - applies to the NEXT field that comes after this comment
|
|
754
|
+
metadata Json?
|
|
755
|
+
|
|
756
|
+
createdAt DateTime @default(now())
|
|
757
|
+
updatedAt DateTime @updatedAt
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Three ways to enable search:**
|
|
762
|
+
|
|
763
|
+
1. **Inline comment**: `title String // @enableSearch`
|
|
764
|
+
2. **Standalone comment**: Place `// @enableSearch` on a line above the field
|
|
765
|
+
3. **Directive comment**: Place `/// @enableSearch` on a line, applies to the next field definition
|
|
766
|
+
|
|
767
|
+
#### Search Methods
|
|
768
|
+
|
|
769
|
+
Every searchable model provides comprehensive search functionality:
|
|
770
|
+
|
|
771
|
+
```tsx
|
|
772
|
+
const {
|
|
773
|
+
data: posts,
|
|
774
|
+
search: searchPosts,
|
|
775
|
+
loading,
|
|
776
|
+
error
|
|
777
|
+
} = useSuparisma.post();
|
|
778
|
+
|
|
779
|
+
// The search object provides:
|
|
780
|
+
// - queries: SearchQuery[] // Current active search queries
|
|
781
|
+
// - loading: boolean // Search loading state
|
|
782
|
+
// - setQueries: (queries) => void // Set multiple search queries
|
|
783
|
+
// - addQuery: (query) => void // Add a single search query
|
|
784
|
+
// - removeQuery: (field) => void // Remove search by field
|
|
785
|
+
// - clearQueries: () => void // Clear all searches
|
|
786
|
+
// - searchMultiField: (value) => void // Search across all searchable fields
|
|
787
|
+
// - searchField: (field, value) => void // Search specific field
|
|
788
|
+
// - getCurrentSearchTerms: () => string[] // Get terms for highlighting
|
|
789
|
+
// - escapeRegex: (text) => string // Safely escape special characters
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
#### Basic Search Examples
|
|
793
|
+
|
|
794
|
+
```tsx
|
|
795
|
+
import useSuparisma from '../generated';
|
|
796
|
+
|
|
797
|
+
function PostSearch() {
|
|
798
|
+
const { data: posts, search: searchPosts } = useSuparisma.post();
|
|
799
|
+
|
|
800
|
+
return (
|
|
801
|
+
<div>
|
|
802
|
+
{/* Search in a specific field */}
|
|
803
|
+
<input
|
|
804
|
+
placeholder="Search titles..."
|
|
805
|
+
onChange={(e) => {
|
|
806
|
+
if (e.target.value.trim()) {
|
|
807
|
+
searchPosts.searchField("title", e.target.value);
|
|
808
|
+
} else {
|
|
809
|
+
searchPosts.clearQueries();
|
|
810
|
+
}
|
|
811
|
+
}}
|
|
812
|
+
/>
|
|
813
|
+
|
|
814
|
+
{/* Search across all searchable fields */}
|
|
815
|
+
<input
|
|
816
|
+
placeholder="Search everywhere..."
|
|
817
|
+
onChange={(e) => {
|
|
818
|
+
if (e.target.value.trim()) {
|
|
819
|
+
searchPosts.searchMultiField(e.target.value);
|
|
820
|
+
} else {
|
|
821
|
+
searchPosts.clearQueries();
|
|
822
|
+
}
|
|
823
|
+
}}
|
|
824
|
+
/>
|
|
825
|
+
|
|
826
|
+
{/* Display results */}
|
|
827
|
+
{posts?.map(post => (
|
|
828
|
+
<div key={post.id}>
|
|
829
|
+
<h3>{post.title}</h3>
|
|
830
|
+
<p>{post.content}</p>
|
|
831
|
+
</div>
|
|
832
|
+
))}
|
|
833
|
+
</div>
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
#### JSON Field Search
|
|
839
|
+
|
|
840
|
+
Suparisma supports full-text search on JSON fields when annotated with `/// @enableSearch`:
|
|
841
|
+
|
|
842
|
+
```prisma
|
|
843
|
+
model Document {
|
|
844
|
+
id String @id @default(uuid())
|
|
845
|
+
title String // @enableSearch
|
|
846
|
+
|
|
847
|
+
/// @enableSearch - Enable search on the next JSON field
|
|
848
|
+
metadata Json? // Will be searchable as text
|
|
849
|
+
|
|
850
|
+
/// @enableSearch
|
|
851
|
+
content Json? // Complex JSON structures are converted to searchable text
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
JSON fields are automatically converted to searchable text during indexing:
|
|
856
|
+
|
|
857
|
+
```tsx
|
|
858
|
+
function DocumentSearch() {
|
|
859
|
+
const { data: documents, search } = useSuparisma.document();
|
|
860
|
+
|
|
861
|
+
return (
|
|
862
|
+
<div>
|
|
863
|
+
{/* Search within JSON metadata */}
|
|
864
|
+
<input
|
|
865
|
+
placeholder="Search metadata..."
|
|
866
|
+
onChange={(e) => {
|
|
867
|
+
if (e.target.value.trim()) {
|
|
868
|
+
search.searchField("metadata", e.target.value);
|
|
869
|
+
} else {
|
|
870
|
+
search.clearQueries();
|
|
871
|
+
}
|
|
872
|
+
}}
|
|
873
|
+
/>
|
|
874
|
+
|
|
875
|
+
{/* Search across all fields including JSON */}
|
|
876
|
+
<input
|
|
877
|
+
placeholder="Search everything (including JSON)..."
|
|
878
|
+
onChange={(e) => {
|
|
879
|
+
if (e.target.value.trim()) {
|
|
880
|
+
search.searchMultiField(e.target.value);
|
|
881
|
+
} else {
|
|
882
|
+
search.clearQueries();
|
|
883
|
+
}
|
|
884
|
+
}}
|
|
885
|
+
/>
|
|
886
|
+
|
|
887
|
+
{documents?.map(doc => (
|
|
888
|
+
<div key={doc.id}>
|
|
889
|
+
<h3>{doc.title}</h3>
|
|
890
|
+
<pre>{JSON.stringify(doc.metadata, null, 2)}</pre>
|
|
891
|
+
</div>
|
|
892
|
+
))}
|
|
893
|
+
</div>
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
**JSON Search Examples:**
|
|
899
|
+
```tsx
|
|
900
|
+
// Search for documents with specific metadata values
|
|
901
|
+
search.searchField("metadata", "author"); // Finds JSON containing "author"
|
|
902
|
+
search.searchField("content", "typescript"); // Finds JSON containing "typescript"
|
|
903
|
+
|
|
904
|
+
// Multi-field search includes JSON fields
|
|
905
|
+
search.searchMultiField("react tutorial"); // Searches title, metadata, content
|
|
906
|
+
```
|
|
626
907
|
|
|
627
|
-
|
|
908
|
+
#### Advanced Search Features
|
|
628
909
|
|
|
910
|
+
**Multi-word Search with AND Logic**
|
|
629
911
|
```tsx
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
912
|
+
// Searching "react typescript" finds posts containing BOTH words
|
|
913
|
+
searchPosts.searchField("title", "react typescript");
|
|
914
|
+
// Internally converts to: "react & typescript" for PostgreSQL
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
**Search Highlighting**
|
|
918
|
+
```tsx
|
|
919
|
+
function PostList() {
|
|
920
|
+
const { data: posts, search } = useSuparisma.post();
|
|
921
|
+
|
|
922
|
+
// Get current search terms for highlighting
|
|
923
|
+
const searchTerms = search.getCurrentSearchTerms();
|
|
924
|
+
|
|
925
|
+
const highlightText = (text: string, searchTerm: string) => {
|
|
926
|
+
if (!searchTerm) return text;
|
|
927
|
+
|
|
928
|
+
// Use the built-in regex escaping
|
|
929
|
+
const escapedTerm = search.escapeRegex(searchTerm);
|
|
930
|
+
const parts = text.split(new RegExp(`(${escapedTerm})`, 'gi'));
|
|
931
|
+
|
|
932
|
+
return parts.map((part, index) =>
|
|
933
|
+
part.toLowerCase() === searchTerm.toLowerCase() ? (
|
|
934
|
+
<mark key={index} className="bg-yellow-200">{part}</mark>
|
|
935
|
+
) : part
|
|
936
|
+
);
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
return (
|
|
940
|
+
<div>
|
|
941
|
+
{posts?.map(post => (
|
|
942
|
+
<div key={post.id}>
|
|
943
|
+
<h3>
|
|
944
|
+
{searchTerms.length > 0
|
|
945
|
+
? highlightText(post.title, searchTerms[0])
|
|
946
|
+
: post.title
|
|
947
|
+
}
|
|
948
|
+
</h3>
|
|
949
|
+
</div>
|
|
950
|
+
))}
|
|
951
|
+
</div>
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Multiple Search Queries**
|
|
957
|
+
```tsx
|
|
958
|
+
// Search multiple fields simultaneously
|
|
959
|
+
search.setQueries([
|
|
960
|
+
{ field: "title", value: "react" },
|
|
961
|
+
{ field: "content", value: "tutorial" }
|
|
962
|
+
]);
|
|
963
|
+
|
|
964
|
+
// Add individual searches
|
|
965
|
+
search.addQuery({ field: "title", value: "javascript" });
|
|
966
|
+
search.addQuery({ field: "tags", value: "frontend" });
|
|
967
|
+
|
|
968
|
+
// Remove specific search
|
|
969
|
+
search.removeQuery("title");
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
**Search State Management**
|
|
973
|
+
```tsx
|
|
974
|
+
function SearchComponent() {
|
|
975
|
+
const { search } = useSuparisma.post();
|
|
976
|
+
|
|
977
|
+
// Monitor search state
|
|
978
|
+
if (search.loading) {
|
|
979
|
+
return <div>Searching...</div>;
|
|
635
980
|
}
|
|
636
|
-
|
|
981
|
+
|
|
982
|
+
// Display active searches
|
|
983
|
+
if (search.queries.length > 0) {
|
|
984
|
+
return (
|
|
985
|
+
<div>
|
|
986
|
+
<p>Active searches:</p>
|
|
987
|
+
{search.queries.map((query, index) => (
|
|
988
|
+
<span key={index} className="tag">
|
|
989
|
+
{query.field}: "{query.value}"
|
|
990
|
+
<button onClick={() => search.removeQuery(query.field)}>
|
|
991
|
+
×
|
|
992
|
+
</button>
|
|
993
|
+
</span>
|
|
994
|
+
))}
|
|
995
|
+
<button onClick={search.clearQueries}>Clear All</button>
|
|
996
|
+
</div>
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return <div>No active searches</div>;
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
#### Real-World Search Examples
|
|
1005
|
+
|
|
1006
|
+
**E-commerce Product Search**
|
|
1007
|
+
```tsx
|
|
1008
|
+
function ProductSearch() {
|
|
1009
|
+
const { data: products, search } = useSuparisma.product();
|
|
1010
|
+
const [searchType, setSearchType] = useState<'name' | 'description' | 'multi'>('multi');
|
|
1011
|
+
|
|
1012
|
+
const handleSearch = (value: string) => {
|
|
1013
|
+
if (!value.trim()) {
|
|
1014
|
+
search.clearQueries();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
switch (searchType) {
|
|
1019
|
+
case 'name':
|
|
1020
|
+
search.searchField('name', value);
|
|
1021
|
+
break;
|
|
1022
|
+
case 'description':
|
|
1023
|
+
search.searchField('description', value);
|
|
1024
|
+
break;
|
|
1025
|
+
case 'multi':
|
|
1026
|
+
search.searchMultiField(value);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
return (
|
|
1032
|
+
<div>
|
|
1033
|
+
{/* Search type selector */}
|
|
1034
|
+
<div className="search-controls">
|
|
1035
|
+
<select value={searchType} onChange={(e) => setSearchType(e.target.value)}>
|
|
1036
|
+
<option value="multi">Search All Fields</option>
|
|
1037
|
+
<option value="name">Product Name Only</option>
|
|
1038
|
+
<option value="description">Description Only</option>
|
|
1039
|
+
</select>
|
|
1040
|
+
|
|
1041
|
+
<input
|
|
1042
|
+
placeholder={`Search ${searchType === 'multi' ? 'products' : searchType}...`}
|
|
1043
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
1044
|
+
/>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
{/* Results with highlighting */}
|
|
1048
|
+
<div className="results">
|
|
1049
|
+
{search.loading && <div>Searching...</div>}
|
|
1050
|
+
{products?.map(product => (
|
|
1051
|
+
<ProductCard key={product.id} product={product} />
|
|
1052
|
+
))}
|
|
1053
|
+
</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
**Content Management Search**
|
|
1060
|
+
```tsx
|
|
1061
|
+
function ArticleSearch() {
|
|
1062
|
+
const { data: articles, search } = useSuparisma.article();
|
|
1063
|
+
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
|
1064
|
+
|
|
1065
|
+
const performSearch = (term: string) => {
|
|
1066
|
+
if (term.trim()) {
|
|
1067
|
+
search.searchMultiField(term);
|
|
1068
|
+
// Add to history (avoid duplicates)
|
|
1069
|
+
setSearchHistory(prev =>
|
|
1070
|
+
[term, ...prev.filter(t => t !== term)].slice(0, 5)
|
|
1071
|
+
);
|
|
1072
|
+
} else {
|
|
1073
|
+
search.clearQueries();
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
return (
|
|
1078
|
+
<div>
|
|
1079
|
+
<div className="search-box">
|
|
1080
|
+
<input
|
|
1081
|
+
placeholder="Search articles..."
|
|
1082
|
+
onChange={(e) => performSearch(e.target.value)}
|
|
1083
|
+
onKeyDown={(e) => {
|
|
1084
|
+
if (e.key === 'Escape') {
|
|
1085
|
+
e.currentTarget.value = '';
|
|
1086
|
+
search.clearQueries();
|
|
1087
|
+
}
|
|
1088
|
+
}}
|
|
1089
|
+
/>
|
|
1090
|
+
|
|
1091
|
+
{/* Search suggestions from history */}
|
|
1092
|
+
{searchHistory.length > 0 && (
|
|
1093
|
+
<div className="search-history">
|
|
1094
|
+
<small>Recent searches:</small>
|
|
1095
|
+
{searchHistory.map((term, index) => (
|
|
1096
|
+
<button
|
|
1097
|
+
key={index}
|
|
1098
|
+
onClick={() => performSearch(term)}
|
|
1099
|
+
className="suggestion"
|
|
1100
|
+
>
|
|
1101
|
+
{term}
|
|
1102
|
+
</button>
|
|
1103
|
+
))}
|
|
1104
|
+
</div>
|
|
1105
|
+
)}
|
|
1106
|
+
</div>
|
|
1107
|
+
|
|
1108
|
+
{/* Search stats */}
|
|
1109
|
+
{search.queries.length > 0 && (
|
|
1110
|
+
<div className="search-stats">
|
|
1111
|
+
Found {articles?.length || 0} articles for "{search.getCurrentSearchTerms().join(', ')}"
|
|
1112
|
+
{search.loading && <span> (searching...)</span>}
|
|
1113
|
+
</div>
|
|
1114
|
+
)}
|
|
1115
|
+
</div>
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
#### Search Implementation Details
|
|
1121
|
+
|
|
1122
|
+
**PostgreSQL Full-Text Search**
|
|
1123
|
+
- Uses `to_tsvector` and `to_tsquery` for efficient full-text search
|
|
1124
|
+
- Automatically creates GIN indexes for searchable fields (recommended)
|
|
1125
|
+
- Supports partial matching with prefix search (`:*`)
|
|
1126
|
+
- Multi-word queries use AND logic (`&`) for better precision
|
|
1127
|
+
|
|
1128
|
+
**Generated RPC Functions**
|
|
1129
|
+
Suparisma automatically generates PostgreSQL RPC functions for search:
|
|
1130
|
+
- `search_{model}_by_{field}_prefix` - Single field search
|
|
1131
|
+
- `search_{model}_multi_field` - Multi-field search
|
|
1132
|
+
|
|
1133
|
+
**Performance Considerations**
|
|
1134
|
+
- Search queries are debounced (300ms) to prevent excessive API calls
|
|
1135
|
+
- Results are cached and updated via realtime subscriptions
|
|
1136
|
+
- Large datasets benefit from database-level GIN indexes:
|
|
1137
|
+
|
|
1138
|
+
```sql
|
|
1139
|
+
-- Recommended indexes for better search performance
|
|
1140
|
+
CREATE INDEX IF NOT EXISTS idx_posts_title_gin
|
|
1141
|
+
ON posts USING gin(to_tsvector('english', title));
|
|
1142
|
+
|
|
1143
|
+
CREATE INDEX IF NOT EXISTS idx_posts_content_gin
|
|
1144
|
+
ON posts USING gin(to_tsvector('english', content));
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
**Error Handling**
|
|
1148
|
+
```tsx
|
|
1149
|
+
function SearchWithErrorHandling() {
|
|
1150
|
+
const { data, search, error } = useSuparisma.post();
|
|
1151
|
+
|
|
1152
|
+
useEffect(() => {
|
|
1153
|
+
if (error) {
|
|
1154
|
+
console.error('Search error:', error);
|
|
1155
|
+
// Fallback to basic filtering
|
|
1156
|
+
search.clearQueries();
|
|
1157
|
+
}
|
|
1158
|
+
}, [error]);
|
|
1159
|
+
|
|
1160
|
+
// Component implementation...
|
|
1161
|
+
}
|
|
637
1162
|
```
|
|
638
1163
|
|
|
639
1164
|
## Schema Annotations
|
|
@@ -652,18 +1177,24 @@ model AuditLog {
|
|
|
652
1177
|
|
|
653
1178
|
model Thing {
|
|
654
1179
|
id String @id @default(uuid())
|
|
655
|
-
name String? // @enableSearch - Enable full-text search for this field
|
|
656
|
-
|
|
1180
|
+
name String? // @enableSearch - Enable full-text search for this field (inline)
|
|
1181
|
+
// @enableSearch - Enable search for the field above (standalone)
|
|
1182
|
+
description String?
|
|
657
1183
|
someNumber Int
|
|
1184
|
+
|
|
1185
|
+
/// @enableSearch - Enable search for the NEXT field that comes after this comment
|
|
1186
|
+
metadata Json?
|
|
658
1187
|
}
|
|
659
1188
|
```
|
|
660
1189
|
|
|
661
1190
|
Available annotations:
|
|
662
1191
|
|
|
663
|
-
| Annotation | Description | Location |
|
|
664
|
-
|
|
665
|
-
| `@disableRealtime` | Disables real-time updates for this model | Model (before definition) |
|
|
666
|
-
|
|
|
1192
|
+
| Annotation | Description | Location | Example |
|
|
1193
|
+
|------------|-------------|----------|---------|
|
|
1194
|
+
| `@disableRealtime` | Disables real-time updates for this model | Model (before definition) | `// @disableRealtime`<br>`model AuditLog { ... }` |
|
|
1195
|
+
| `// @enableSearch` | Enables full-text search (inline) | Field (after definition) | `name String // @enableSearch` |
|
|
1196
|
+
| `// @enableSearch` | Enables full-text search (standalone) | Line above field | `// @enableSearch`<br>`name String` |
|
|
1197
|
+
| `/// @enableSearch` | Enables full-text search (directive) | Applies to next field | `/// @enableSearch`<br>`metadata Json?` |
|
|
667
1198
|
|
|
668
1199
|
## Building UI Components
|
|
669
1200
|
|
|
@@ -866,10 +1397,13 @@ export default function ThingTable() {
|
|
|
866
1397
|
|----------|----------|-------------|---------|
|
|
867
1398
|
| `DATABASE_URL` | Yes | Postgres database URL used by Prisma | `postgresql://user:pass@host:port/db` |
|
|
868
1399
|
| `DIRECT_URL` | Yes | Direct URL to Postgres DB for realtime setup | `postgresql://user:pass@host:port/db` |
|
|
869
|
-
| `NEXT_PUBLIC_SUPABASE_URL` | Yes | Your Supabase project URL | `https://xyz.supabase.co` |
|
|
870
|
-
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes | Supabase anonymous key | `eyJh...` |
|
|
1400
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Yes (Web) | Your Supabase project URL (Next.js) | `https://xyz.supabase.co` |
|
|
1401
|
+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes (Web) | Supabase anonymous key (Next.js) | `eyJh...` |
|
|
1402
|
+
| `EXPO_PUBLIC_SUPABASE_URL` | Yes (RN) | Your Supabase project URL (Expo) | `https://xyz.supabase.co` |
|
|
1403
|
+
| `EXPO_PUBLIC_SUPABASE_ANON_KEY` | Yes (RN) | Supabase anonymous key (Expo) | `eyJh...` |
|
|
871
1404
|
| `SUPARISMA_OUTPUT_DIR` | No | Custom output directory | `src/lib/suparisma` |
|
|
872
1405
|
| `SUPARISMA_PRISMA_SCHEMA_PATH` | No | Custom schema path | `db/schema.prisma` |
|
|
1406
|
+
| `SUPARISMA_PLATFORM` | No | Target platform: `web` or `react-native` | `react-native` |
|
|
873
1407
|
|
|
874
1408
|
### CLI Commands
|
|
875
1409
|
|
|
@@ -1039,6 +1573,12 @@ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon;
|
|
|
1039
1573
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
1040
1574
|
GRANT USAGE, SELECT ON SEQUENCES TO anon;
|
|
1041
1575
|
```
|
|
1576
|
+
|
|
1577
|
+
You could also try debugging on a table, the following is NOT recommended but you can debug permissions given to anon, service_account and give all access to anon key to make sure that's not the issue:
|
|
1578
|
+
```sql
|
|
1579
|
+
GRANT ALL ON TABLE "(tableName)" TO anon;
|
|
1580
|
+
GRANT ALL ON TABLE "(tableName)" TO authenticated;
|
|
1581
|
+
```
|
|
1042
1582
|
|
|
1043
1583
|
**"Unknown command: undefined"**
|
|
1044
1584
|
|