suparisma 1.2.1 → 1.2.3
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
|
@@ -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)
|
|
@@ -25,6 +26,8 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
|
|
|
25
26
|
- [Array Filtering](#array-filtering)
|
|
26
27
|
- [Sorting Data](#sorting-data)
|
|
27
28
|
- [Pagination](#pagination)
|
|
29
|
+
- [Field Selection (select)](#field-selection-select)
|
|
30
|
+
- [Including Relations (include)](#including-relations-include)
|
|
28
31
|
- [Search Functionality](#search-functionality)
|
|
29
32
|
- [Enabling Search](#enabling-search)
|
|
30
33
|
- [Search Methods](#search-methods)
|
|
@@ -60,7 +63,7 @@ Suparisma bridges this gap by:
|
|
|
60
63
|
- Enabling easy **pagination, filtering, and search** on your data
|
|
61
64
|
- Leveraging both **Prisma** and **Supabase** official SDKs
|
|
62
65
|
- Respecting **Supabase's auth rules** for secure database access
|
|
63
|
-
- Working seamlessly with any React environment (Next.js, Remix, Tanstack Router, etc.)
|
|
66
|
+
- Working seamlessly with any React environment (Next.js, Remix, Tanstack Router, React Native, etc.)
|
|
64
67
|
|
|
65
68
|
## Features
|
|
66
69
|
|
|
@@ -70,7 +73,7 @@ Suparisma bridges this gap by:
|
|
|
70
73
|
- 🔍 **Full-text search** with configurable annotations *(currently under maintenance)*
|
|
71
74
|
- 🔢 **Pagination and sorting** built into every hook
|
|
72
75
|
- 🧩 **Prisma-like API** that feels familiar if you already use Prisma
|
|
73
|
-
- 📱 **Works with any React framework** including Next.js, Remix,
|
|
76
|
+
- 📱 **Works with any React framework** including Next.js, Remix, React Native, and Expo
|
|
74
77
|
- 🛠️ **Simple CLI** to generate hooks with a single command
|
|
75
78
|
|
|
76
79
|
## Installation
|
|
@@ -86,6 +89,110 @@ yarn add suparisma
|
|
|
86
89
|
pnpm install suparisma
|
|
87
90
|
```
|
|
88
91
|
|
|
92
|
+
## React Native / Expo Setup
|
|
93
|
+
|
|
94
|
+
Suparisma fully supports React Native and Expo projects. Follow these additional steps for mobile development:
|
|
95
|
+
|
|
96
|
+
### 1. Install Dependencies
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Install Suparisma and required dependencies
|
|
100
|
+
pnpm install suparisma @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
|
|
101
|
+
|
|
102
|
+
# For UUID generation support (recommended)
|
|
103
|
+
pnpm install react-native-get-random-values
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Add Polyfills
|
|
107
|
+
|
|
108
|
+
Add these imports at the very top of your app's entry point (e.g., `App.tsx` or `index.js`):
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// App.tsx or index.js - Add these at the VERY TOP before any other imports
|
|
112
|
+
import 'react-native-get-random-values'; // Required for UUID generation
|
|
113
|
+
import 'react-native-url-polyfill/auto'; // Required for Supabase
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Set Environment Variables
|
|
117
|
+
|
|
118
|
+
For **Expo** projects, use the `EXPO_PUBLIC_` prefix in your `.env` file:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
EXPO_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
122
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For **bare React Native** projects, use `react-native-dotenv` or similar.
|
|
126
|
+
|
|
127
|
+
### 4. Generate Hooks for React Native
|
|
128
|
+
|
|
129
|
+
Set the `SUPARISMA_PLATFORM` environment variable when generating:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Generate hooks for React Native / Expo
|
|
133
|
+
SUPARISMA_PLATFORM=react-native npx suparisma generate
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or add it to your `package.json` scripts:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"scripts": {
|
|
141
|
+
"suparisma:generate": "SUPARISMA_PLATFORM=react-native npx suparisma generate"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 5. Use the Hooks
|
|
147
|
+
|
|
148
|
+
The hooks work exactly the same as in web projects:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import React from 'react';
|
|
152
|
+
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
|
153
|
+
import useSuparisma from './src/suparisma/generated';
|
|
154
|
+
|
|
155
|
+
function ThingList() {
|
|
156
|
+
const {
|
|
157
|
+
data: things,
|
|
158
|
+
loading,
|
|
159
|
+
error,
|
|
160
|
+
create: createThing
|
|
161
|
+
} = useSuparisma.thing();
|
|
162
|
+
|
|
163
|
+
if (loading) return <Text>Loading...</Text>;
|
|
164
|
+
if (error) return <Text>Error: {error.message}</Text>;
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<View>
|
|
168
|
+
<FlatList
|
|
169
|
+
data={things}
|
|
170
|
+
keyExtractor={(item) => item.id}
|
|
171
|
+
renderItem={({ item }) => (
|
|
172
|
+
<Text>{item.name} (Number: {item.someNumber})</Text>
|
|
173
|
+
)}
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
<TouchableOpacity
|
|
177
|
+
onPress={() => createThing({
|
|
178
|
+
name: "New Thing",
|
|
179
|
+
someNumber: Math.floor(Math.random() * 100)
|
|
180
|
+
})}
|
|
181
|
+
>
|
|
182
|
+
<Text>Add Thing</Text>
|
|
183
|
+
</TouchableOpacity>
|
|
184
|
+
</View>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Platform Detection
|
|
190
|
+
|
|
191
|
+
The generated Supabase client automatically configures itself for React Native with:
|
|
192
|
+
- **AsyncStorage** for auth persistence
|
|
193
|
+
- **Session detection** disabled (not applicable in mobile)
|
|
194
|
+
- **Auto refresh token** enabled
|
|
195
|
+
|
|
89
196
|
## Quick Start
|
|
90
197
|
|
|
91
198
|
1. **Add a Prisma schema**: Ensure you have a valid `prisma/schema.prisma` file in your project
|
|
@@ -139,6 +246,7 @@ Note: you can adjust the prisma schema path and the generated files output path
|
|
|
139
246
|
```bash
|
|
140
247
|
SUPARISMA_PRISMA_SCHEMA_PATH="./prisma/schema.prisma"
|
|
141
248
|
SUPARISMA_OUTPUT_DIR="./src/suparisma/generated"
|
|
249
|
+
SUPARISMA_PLATFORM="web" # or "react-native" for React Native/Expo projects
|
|
142
250
|
```
|
|
143
251
|
Also make sure to not change any of these generated files directly as **they will always be overwritten**
|
|
144
252
|
|
|
@@ -627,6 +735,53 @@ const { data: page2 } = useSuparisma.thing({
|
|
|
627
735
|
const { data, count } = useSuparisma.thing();
|
|
628
736
|
```
|
|
629
737
|
|
|
738
|
+
### Field Selection (select)
|
|
739
|
+
|
|
740
|
+
Use the `select` option to only return specific fields, reducing payload size:
|
|
741
|
+
|
|
742
|
+
```tsx
|
|
743
|
+
// Only get id and name fields
|
|
744
|
+
const { data: things } = useSuparisma.thing({
|
|
745
|
+
select: { id: true, name: true }
|
|
746
|
+
});
|
|
747
|
+
// Returns: [{ id: "123", name: "Thing 1" }, ...]
|
|
748
|
+
|
|
749
|
+
// Combine with filtering
|
|
750
|
+
const { data: activeThings } = useSuparisma.thing({
|
|
751
|
+
where: { someEnum: "ONE" },
|
|
752
|
+
select: { id: true, name: true, someNumber: true }
|
|
753
|
+
});
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Including Relations (include)
|
|
757
|
+
|
|
758
|
+
Use the `include` option to fetch related records (foreign key relations):
|
|
759
|
+
|
|
760
|
+
```tsx
|
|
761
|
+
// Include all fields from a related model
|
|
762
|
+
const { data: posts } = useSuparisma.post({
|
|
763
|
+
include: { author: true }
|
|
764
|
+
});
|
|
765
|
+
// Returns: [{ id: "123", title: "...", author: { id: "456", name: "John" } }, ...]
|
|
766
|
+
|
|
767
|
+
// Include specific fields from a relation
|
|
768
|
+
const { data: posts } = useSuparisma.post({
|
|
769
|
+
include: {
|
|
770
|
+
author: {
|
|
771
|
+
select: { id: true, name: true }
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Combine select and include
|
|
777
|
+
const { data: posts } = useSuparisma.post({
|
|
778
|
+
select: { id: true, title: true },
|
|
779
|
+
include: { author: true, comments: true }
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Note:** The relation names in `include` should match your Prisma schema relation field names.
|
|
784
|
+
|
|
630
785
|
### Search Functionality
|
|
631
786
|
|
|
632
787
|
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.
|
|
@@ -1291,10 +1446,13 @@ export default function ThingTable() {
|
|
|
1291
1446
|
|----------|----------|-------------|---------|
|
|
1292
1447
|
| `DATABASE_URL` | Yes | Postgres database URL used by Prisma | `postgresql://user:pass@host:port/db` |
|
|
1293
1448
|
| `DIRECT_URL` | Yes | Direct URL to Postgres DB for realtime setup | `postgresql://user:pass@host:port/db` |
|
|
1294
|
-
| `NEXT_PUBLIC_SUPABASE_URL` | Yes | Your Supabase project URL | `https://xyz.supabase.co` |
|
|
1295
|
-
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes | Supabase anonymous key | `eyJh...` |
|
|
1449
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Yes (Web) | Your Supabase project URL (Next.js) | `https://xyz.supabase.co` |
|
|
1450
|
+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes (Web) | Supabase anonymous key (Next.js) | `eyJh...` |
|
|
1451
|
+
| `EXPO_PUBLIC_SUPABASE_URL` | Yes (RN) | Your Supabase project URL (Expo) | `https://xyz.supabase.co` |
|
|
1452
|
+
| `EXPO_PUBLIC_SUPABASE_ANON_KEY` | Yes (RN) | Supabase anonymous key (Expo) | `eyJh...` |
|
|
1296
1453
|
| `SUPARISMA_OUTPUT_DIR` | No | Custom output directory | `src/lib/suparisma` |
|
|
1297
1454
|
| `SUPARISMA_PRISMA_SCHEMA_PATH` | No | Custom schema path | `db/schema.prisma` |
|
|
1455
|
+
| `SUPARISMA_PLATFORM` | No | Target platform: `web` or `react-native` | `react-native` |
|
|
1298
1456
|
|
|
1299
1457
|
### CLI Commands
|
|
1300
1458
|
|
|
@@ -1416,8 +1574,8 @@ const { data } = useSuparisma.thing({
|
|
|
1416
1574
|
| `limit` | `number` | Maximum number of records to return |
|
|
1417
1575
|
| `offset` | `number` | Number of records to skip for pagination |
|
|
1418
1576
|
| `realtime` | `boolean` | Enable/disable real-time updates |
|
|
1419
|
-
| `select` | `object` | Fields to include in the response |
|
|
1420
|
-
| `include` | `object` | Related records to include |
|
|
1577
|
+
| `select` | `object` | Fields to include in the response. Use `{ fieldName: true }` syntax |
|
|
1578
|
+
| `include` | `object` | Related records to include. Use `{ relationName: true }` or `{ relationName: { select: {...} } }` |
|
|
1421
1579
|
| `search` | `object` | Full-text search configuration |
|
|
1422
1580
|
|
|
1423
1581
|
### Hook Return Value
|
|
@@ -1464,6 +1622,12 @@ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon;
|
|
|
1464
1622
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
1465
1623
|
GRANT USAGE, SELECT ON SEQUENCES TO anon;
|
|
1466
1624
|
```
|
|
1625
|
+
|
|
1626
|
+
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:
|
|
1627
|
+
```sql
|
|
1628
|
+
GRANT ALL ON TABLE "(tableName)" TO anon;
|
|
1629
|
+
GRANT ALL ON TABLE "(tableName)" TO authenticated;
|
|
1630
|
+
```
|
|
1467
1631
|
|
|
1468
1632
|
**"Unknown command: undefined"**
|
|
1469
1633
|
|
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';
|
|
@@ -47,6 +47,42 @@ export function escapeRegexCharacters(str: string): string {
|
|
|
47
47
|
return str.replace(/[()\\[\\]{}+*?^$|.\\\\]/g, '\\\\\\\\$&');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Generate a UUID v4, with fallback for environments without crypto.randomUUID()
|
|
52
|
+
* Works in: browsers, Node.js, and React Native (with react-native-get-random-values polyfill)
|
|
53
|
+
*
|
|
54
|
+
* For React Native, ensure you have installed and imported the polyfill:
|
|
55
|
+
* - pnpm install react-native-get-random-values
|
|
56
|
+
* - Import at app entry point: import 'react-native-get-random-values';
|
|
57
|
+
*/
|
|
58
|
+
export function generateUUID(): string {
|
|
59
|
+
// Try native crypto.randomUUID() first (modern browsers & Node.js 16.7+)
|
|
60
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
61
|
+
return crypto.randomUUID();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback using crypto.getRandomValues() (works with react-native-get-random-values polyfill)
|
|
65
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
66
|
+
const bytes = new Uint8Array(16);
|
|
67
|
+
crypto.getRandomValues(bytes);
|
|
68
|
+
|
|
69
|
+
// Set version (4) and variant (RFC 4122)
|
|
70
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
71
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
|
|
72
|
+
|
|
73
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
74
|
+
return \`\${hex.slice(0, 8)}-\${hex.slice(8, 12)}-\${hex.slice(12, 16)}-\${hex.slice(16, 20)}-\${hex.slice(20)}\`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Last resort fallback using Math.random() (not cryptographically secure)
|
|
78
|
+
console.warn('[Suparisma] crypto API not available, using Math.random() fallback for UUID generation');
|
|
79
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
80
|
+
const r = (Math.random() * 16) | 0;
|
|
81
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
82
|
+
return v.toString(16);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
/**
|
|
51
87
|
* Advanced filter operators for complex queries
|
|
52
88
|
* @example
|
|
@@ -133,7 +169,36 @@ export type AdvancedWhereInput<T> = {
|
|
|
133
169
|
* limit: 10
|
|
134
170
|
* });
|
|
135
171
|
*/
|
|
136
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Select input type - specify which fields to return
|
|
174
|
+
* Use true to include a field, or use an object for relations
|
|
175
|
+
* @example
|
|
176
|
+
* // Select specific fields
|
|
177
|
+
* { id: true, name: true, email: true }
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* // Select fields with relations
|
|
181
|
+
* { id: true, name: true, posts: true }
|
|
182
|
+
*/
|
|
183
|
+
export type SelectInput<T> = {
|
|
184
|
+
[K in keyof T]?: boolean;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Include input type - specify which relations to include
|
|
189
|
+
* @example
|
|
190
|
+
* // Include a relation with all fields
|
|
191
|
+
* { posts: true }
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* // Include a relation with specific fields
|
|
195
|
+
* { posts: { select: { id: true, title: true } } }
|
|
196
|
+
*/
|
|
197
|
+
export type IncludeInput = {
|
|
198
|
+
[key: string]: boolean | { select?: Record<string, boolean> };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export type SuparismaOptions<TWhereInput, TOrderByInput, TSelectInput = Record<string, boolean>> = {
|
|
137
202
|
/** Whether to enable realtime updates (default: true) */
|
|
138
203
|
realtime?: boolean;
|
|
139
204
|
/** Custom channel name for realtime subscription */
|
|
@@ -148,6 +213,16 @@ export type SuparismaOptions<TWhereInput, TOrderByInput> = {
|
|
|
148
213
|
limit?: number;
|
|
149
214
|
/** Offset for pagination (skip records) */
|
|
150
215
|
offset?: number;
|
|
216
|
+
/**
|
|
217
|
+
* Select specific fields to return. Reduces payload size.
|
|
218
|
+
* @example { id: true, name: true, email: true }
|
|
219
|
+
*/
|
|
220
|
+
select?: TSelectInput;
|
|
221
|
+
/**
|
|
222
|
+
* Include related records (foreign key relations).
|
|
223
|
+
* @example { posts: true } or { posts: { select: { id: true, title: true } } }
|
|
224
|
+
*/
|
|
225
|
+
include?: IncludeInput;
|
|
151
226
|
};
|
|
152
227
|
|
|
153
228
|
/**
|
|
@@ -697,6 +772,82 @@ function matchesFilter<T>(record: any, filter: T): boolean {
|
|
|
697
772
|
return conditions.every(condition => condition);
|
|
698
773
|
}
|
|
699
774
|
|
|
775
|
+
/**
|
|
776
|
+
* Build a Supabase select string from select and include options.
|
|
777
|
+
*
|
|
778
|
+
* @param select - Object specifying which fields to select { field: true }
|
|
779
|
+
* @param include - Object specifying which relations to include { relation: true }
|
|
780
|
+
* @returns A Supabase-compatible select string
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* // Select specific fields
|
|
784
|
+
* buildSelectString({ id: true, name: true }) // Returns "id,name"
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* // Include relations
|
|
788
|
+
* buildSelectString(undefined, { posts: true }) // Returns "*,posts(*)"
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* // Select fields and include relations with specific fields
|
|
792
|
+
* buildSelectString({ id: true, name: true }, { posts: { select: { id: true, title: true } } })
|
|
793
|
+
* // Returns "id,name,posts(id,title)"
|
|
794
|
+
*/
|
|
795
|
+
export function buildSelectString<TSelect, TInclude>(
|
|
796
|
+
select?: TSelect,
|
|
797
|
+
include?: TInclude
|
|
798
|
+
): string {
|
|
799
|
+
const parts: string[] = [];
|
|
800
|
+
|
|
801
|
+
// Handle select - if provided, only return specified fields
|
|
802
|
+
if (select && typeof select === 'object') {
|
|
803
|
+
const selectedFields = Object.entries(select)
|
|
804
|
+
.filter(([_, value]) => value === true)
|
|
805
|
+
.map(([key]) => key);
|
|
806
|
+
|
|
807
|
+
if (selectedFields.length > 0) {
|
|
808
|
+
parts.push(...selectedFields);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Handle include - add related records
|
|
813
|
+
if (include && typeof include === 'object') {
|
|
814
|
+
for (const [relationName, relationValue] of Object.entries(include)) {
|
|
815
|
+
if (relationValue === true) {
|
|
816
|
+
// Include all fields from the relation
|
|
817
|
+
parts.push(\`\${relationName}(*)\`);
|
|
818
|
+
} else if (typeof relationValue === 'object' && relationValue !== null) {
|
|
819
|
+
// Include specific fields from the relation
|
|
820
|
+
const relationOptions = relationValue as { select?: Record<string, boolean> };
|
|
821
|
+
if (relationOptions.select) {
|
|
822
|
+
const relationFields = Object.entries(relationOptions.select)
|
|
823
|
+
.filter(([_, value]) => value === true)
|
|
824
|
+
.map(([key]) => key);
|
|
825
|
+
|
|
826
|
+
if (relationFields.length > 0) {
|
|
827
|
+
parts.push(\`\${relationName}(\${relationFields.join(',')})\`);
|
|
828
|
+
} else {
|
|
829
|
+
parts.push(\`\${relationName}(*)\`);
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
parts.push(\`\${relationName}(*)\`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// If no select specified but include is, we need to include base table fields too
|
|
839
|
+
if (parts.length === 0) {
|
|
840
|
+
return '*';
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// If only include was specified (no select), we need all base fields plus relations
|
|
844
|
+
if (!select && include) {
|
|
845
|
+
return '*,' + parts.join(',');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return parts.join(',');
|
|
849
|
+
}
|
|
850
|
+
|
|
700
851
|
/**
|
|
701
852
|
* Apply order by to the query builder
|
|
702
853
|
*/
|
|
@@ -792,13 +943,19 @@ export function createSuparismaHook<
|
|
|
792
943
|
orderBy,
|
|
793
944
|
limit,
|
|
794
945
|
offset,
|
|
946
|
+
select,
|
|
947
|
+
include,
|
|
795
948
|
} = options;
|
|
796
949
|
|
|
950
|
+
// Build the select string once for reuse
|
|
951
|
+
const selectString = buildSelectString(select, include);
|
|
952
|
+
|
|
797
953
|
// Refs to store the latest options for realtime handlers
|
|
798
954
|
const whereRef = useRef(where);
|
|
799
955
|
const orderByRef = useRef(orderBy);
|
|
800
956
|
const limitRef = useRef(limit);
|
|
801
957
|
const offsetRef = useRef(offset);
|
|
958
|
+
const selectStringRef = useRef(selectString);
|
|
802
959
|
|
|
803
960
|
// Update refs whenever options change
|
|
804
961
|
useEffect(() => {
|
|
@@ -817,6 +974,10 @@ export function createSuparismaHook<
|
|
|
817
974
|
offsetRef.current = offset;
|
|
818
975
|
}, [offset]);
|
|
819
976
|
|
|
977
|
+
useEffect(() => {
|
|
978
|
+
selectStringRef.current = selectString;
|
|
979
|
+
}, [selectString]);
|
|
980
|
+
|
|
820
981
|
// Single data collection for holding results
|
|
821
982
|
const [data, setData] = useState<TWithRelations[]>([]);
|
|
822
983
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -1162,7 +1323,8 @@ export function createSuparismaHook<
|
|
|
1162
1323
|
setLoading(true);
|
|
1163
1324
|
setError(null);
|
|
1164
1325
|
|
|
1165
|
-
|
|
1326
|
+
// Use selectString for field selection (includes relations if specified)
|
|
1327
|
+
let query = supabase.from(tableName).select(selectString);
|
|
1166
1328
|
|
|
1167
1329
|
// Apply where conditions if provided
|
|
1168
1330
|
if (params?.where) {
|
|
@@ -1249,7 +1411,7 @@ export function createSuparismaHook<
|
|
|
1249
1411
|
|
|
1250
1412
|
const { data, error } = await supabase
|
|
1251
1413
|
.from(tableName)
|
|
1252
|
-
.select(
|
|
1414
|
+
.select(selectString)
|
|
1253
1415
|
.eq(primaryKey, value)
|
|
1254
1416
|
.maybeSingle();
|
|
1255
1417
|
|
|
@@ -1572,7 +1734,7 @@ export function createSuparismaHook<
|
|
|
1572
1734
|
}, [realtime, channelName, tableName]); // NEVER include 'where' - subscription should persist
|
|
1573
1735
|
|
|
1574
1736
|
// Create a memoized options object to prevent unnecessary re-renders
|
|
1575
|
-
const optionsRef = useRef({ where, orderBy, limit, offset });
|
|
1737
|
+
const optionsRef = useRef({ where, orderBy, limit, offset, selectString });
|
|
1576
1738
|
|
|
1577
1739
|
// Compare current options with previous options
|
|
1578
1740
|
const optionsChanged = useCallback(() => {
|
|
@@ -1587,16 +1749,17 @@ export function createSuparismaHook<
|
|
|
1587
1749
|
whereStr !== prevWhereStr ||
|
|
1588
1750
|
orderByStr !== prevOrderByStr ||
|
|
1589
1751
|
limit !== optionsRef.current.limit ||
|
|
1590
|
-
offset !== optionsRef.current.offset
|
|
1752
|
+
offset !== optionsRef.current.offset ||
|
|
1753
|
+
selectString !== optionsRef.current.selectString;
|
|
1591
1754
|
|
|
1592
1755
|
if (hasChanged) {
|
|
1593
1756
|
// Update the ref with the new values
|
|
1594
|
-
optionsRef.current = { where, orderBy, limit, offset };
|
|
1757
|
+
optionsRef.current = { where, orderBy, limit, offset, selectString };
|
|
1595
1758
|
return true;
|
|
1596
1759
|
}
|
|
1597
1760
|
|
|
1598
1761
|
return false;
|
|
1599
|
-
}, [where, orderBy, limit, offset]);
|
|
1762
|
+
}, [where, orderBy, limit, offset, selectString]);
|
|
1600
1763
|
|
|
1601
1764
|
// Load initial data and refetch when options change (BUT NEVER TOUCH SUBSCRIPTION)
|
|
1602
1765
|
useEffect(() => {
|
|
@@ -1690,7 +1853,7 @@ export function createSuparismaHook<
|
|
|
1690
1853
|
if (defaultValue.includes('now()') || defaultValue.includes('now')) {
|
|
1691
1854
|
appliedDefaults[field] = now.toISOString(); // Database expects ISO string
|
|
1692
1855
|
} else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
|
|
1693
|
-
appliedDefaults[field] =
|
|
1856
|
+
appliedDefaults[field] = generateUUID();
|
|
1694
1857
|
} else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
|
|
1695
1858
|
// Simple cuid-like implementation for client-side
|
|
1696
1859
|
appliedDefaults[field] = 'c' + Math.random().toString(36).substring(2, 15);
|
|
@@ -1720,7 +1883,7 @@ export function createSuparismaHook<
|
|
|
1720
1883
|
const { data: result, error } = await supabase
|
|
1721
1884
|
.from(tableName)
|
|
1722
1885
|
.insert([itemWithDefaults])
|
|
1723
|
-
.select();
|
|
1886
|
+
.select(selectString);
|
|
1724
1887
|
|
|
1725
1888
|
if (error) throw error;
|
|
1726
1889
|
|
|
@@ -1812,7 +1975,7 @@ export function createSuparismaHook<
|
|
|
1812
1975
|
.from(tableName)
|
|
1813
1976
|
.update(itemWithDefaults)
|
|
1814
1977
|
.eq(primaryKey, value)
|
|
1815
|
-
.select();
|
|
1978
|
+
.select(selectString);
|
|
1816
1979
|
|
|
1817
1980
|
if (error) throw error;
|
|
1818
1981
|
|
|
@@ -1867,7 +2030,7 @@ export function createSuparismaHook<
|
|
|
1867
2030
|
// First fetch the record to return it
|
|
1868
2031
|
const { data: recordToDelete } = await supabase
|
|
1869
2032
|
.from(tableName)
|
|
1870
|
-
.select(
|
|
2033
|
+
.select(selectString)
|
|
1871
2034
|
.eq(primaryKey, value)
|
|
1872
2035
|
.maybeSingle();
|
|
1873
2036
|
|
|
@@ -42,6 +42,8 @@ import type {
|
|
|
42
42
|
${modelName}WhereInput,
|
|
43
43
|
${modelName}WhereUniqueInput,
|
|
44
44
|
${modelName}OrderByInput,
|
|
45
|
+
${modelName}SelectInput,
|
|
46
|
+
${modelName}IncludeInput,
|
|
45
47
|
${modelName}HookApi,
|
|
46
48
|
Use${modelName}Options
|
|
47
49
|
} from '../types/${modelName}Types';
|
|
@@ -92,6 +94,18 @@ import type {
|
|
|
92
94
|
* orderBy: { // ordering },
|
|
93
95
|
* take: 20 // limit
|
|
94
96
|
* });
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Select specific fields only
|
|
100
|
+
* const ${modelName.toLowerCase()} = ${config_1.HOOK_NAME_PREFIX}${modelName}({
|
|
101
|
+
* select: { id: true, name: true }
|
|
102
|
+
* });
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Include related records
|
|
106
|
+
* const ${modelName.toLowerCase()} = ${config_1.HOOK_NAME_PREFIX}${modelName}({
|
|
107
|
+
* include: { relatedModel: true }
|
|
108
|
+
* });
|
|
95
109
|
*/
|
|
96
110
|
export const ${config_1.HOOK_NAME_PREFIX}${modelName} = createSuparismaHook<
|
|
97
111
|
${modelName}WithRelations,
|
|
@@ -6,21 +6,83 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.generateSupabaseClientFile = generateSupabaseClientFile;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const config_1 = require("../config");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
/**
|
|
11
|
+
* Generate the Supabase client file based on the target platform.
|
|
12
|
+
* Supports both web (Next.js, etc.) and React Native/Expo.
|
|
13
|
+
*/
|
|
10
14
|
function generateSupabaseClientFile() {
|
|
11
|
-
|
|
15
|
+
let supabaseClientContent;
|
|
16
|
+
if (config_1.PLATFORM === 'react-native') {
|
|
17
|
+
// React Native / Expo compatible client
|
|
18
|
+
supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
19
|
+
// Platform: React Native / Expo
|
|
20
|
+
//
|
|
21
|
+
// IMPORTANT: Before using Suparisma in React Native, ensure you have:
|
|
22
|
+
// 1. Installed required dependencies:
|
|
23
|
+
// pnpm install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
|
|
24
|
+
//
|
|
25
|
+
// 2. Added polyfills at your app's entry point (e.g., App.tsx or index.js):
|
|
26
|
+
// import 'react-native-url-polyfill/auto';
|
|
27
|
+
//
|
|
28
|
+
// 3. Set your Supabase credentials below or via environment variables
|
|
29
|
+
|
|
30
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
12
31
|
import { createClient } from '@supabase/supabase-js';
|
|
13
32
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
// Option 1: Set your Supabase credentials directly (for quick setup)
|
|
34
|
+
// const SUPABASE_URL = 'https://your-project.supabase.co';
|
|
35
|
+
// const SUPABASE_ANON_KEY = 'your-anon-key';
|
|
36
|
+
|
|
37
|
+
// Option 2: Use environment variables (recommended for production)
|
|
38
|
+
// With Expo, use expo-constants or babel-plugin-inline-dotenv
|
|
39
|
+
// With bare React Native, use react-native-dotenv
|
|
40
|
+
const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || '';
|
|
41
|
+
const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || '';
|
|
42
|
+
|
|
43
|
+
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
44
|
+
console.warn(
|
|
45
|
+
'[Suparisma] Supabase credentials not found. Please set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY ' +
|
|
46
|
+
'in your environment variables, or update the credentials directly in this file.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
51
|
+
auth: {
|
|
52
|
+
storage: AsyncStorage,
|
|
53
|
+
autoRefreshToken: true,
|
|
54
|
+
persistSession: true,
|
|
55
|
+
detectSessionInUrl: false, // Important for React Native
|
|
56
|
+
},
|
|
57
|
+
});
|
|
18
58
|
`;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Web platform (Next.js, Remix, etc.)
|
|
62
|
+
supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
63
|
+
// Platform: Web (Next.js, Remix, etc.)
|
|
64
|
+
import { createClient } from '@supabase/supabase-js';
|
|
65
|
+
|
|
66
|
+
// For Next.js, use NEXT_PUBLIC_ prefix
|
|
67
|
+
// For other frameworks, adjust the environment variable names as needed
|
|
68
|
+
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || '';
|
|
69
|
+
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || '';
|
|
70
|
+
|
|
71
|
+
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
72
|
+
console.warn(
|
|
73
|
+
'[Suparisma] Supabase credentials not found. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY ' +
|
|
74
|
+
'(or SUPABASE_URL and SUPABASE_ANON_KEY) in your environment variables.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
19
81
|
// Output to the UTILS_DIR
|
|
20
82
|
const outputPath = path_1.default.join(config_1.UTILS_DIR, 'supabase-client.ts');
|
|
21
83
|
if (!fs_1.default.existsSync(config_1.UTILS_DIR)) {
|
|
22
84
|
fs_1.default.mkdirSync(config_1.UTILS_DIR, { recursive: true });
|
|
23
85
|
}
|
|
24
86
|
fs_1.default.writeFileSync(outputPath, supabaseClientContent);
|
|
25
|
-
console.log(`🚀 Generated Supabase client file at: ${outputPath}`);
|
|
87
|
+
console.log(`🚀 Generated Supabase client file at: ${outputPath} (platform: ${config_1.PLATFORM})`);
|
|
26
88
|
}
|
|
@@ -352,6 +352,40 @@ export type ${modelName}OrderByInput = {
|
|
|
352
352
|
[key in keyof ${modelName}WithRelations]?: 'asc' | 'desc';
|
|
353
353
|
};
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Select specific fields to return from ${modelName} queries.
|
|
357
|
+
* Set fields to \`true\` to include them in the response.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Only return id and name
|
|
361
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
362
|
+
* select: { id: true, name: true }
|
|
363
|
+
* });
|
|
364
|
+
*/
|
|
365
|
+
export type ${modelName}SelectInput = {
|
|
366
|
+
[key in keyof ${modelName}WithRelations]?: boolean;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Include related records when querying ${modelName}.
|
|
371
|
+
* Set relation names to \`true\` to include all fields, or use an object to select specific fields.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* // Include all fields from a relation
|
|
375
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
376
|
+
* include: { relatedModel: true }
|
|
377
|
+
* });
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* // Include specific fields from a relation
|
|
381
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
382
|
+
* include: { relatedModel: { select: { id: true, name: true } } }
|
|
383
|
+
* });
|
|
384
|
+
*/
|
|
385
|
+
export type ${modelName}IncludeInput = {
|
|
386
|
+
[key: string]: boolean | { select?: Record<string, boolean> };
|
|
387
|
+
};
|
|
388
|
+
|
|
355
389
|
/**
|
|
356
390
|
* Result type for operations that return a single ${modelName} record.
|
|
357
391
|
*/
|
|
@@ -364,8 +398,12 @@ export type ${modelName}ManyResult = ModelResult<${modelName}WithRelations[]>;
|
|
|
364
398
|
|
|
365
399
|
/**
|
|
366
400
|
* Configuration options for the ${modelName} hook.
|
|
401
|
+
* Includes where filters, ordering, pagination, and field selection.
|
|
367
402
|
*/
|
|
368
|
-
export type Use${modelName}Options = SuparismaOptions<${modelName}WhereInput, ${modelName}OrderByInput
|
|
403
|
+
export type Use${modelName}Options = SuparismaOptions<${modelName}WhereInput, ${modelName}OrderByInput, ${modelName}SelectInput> & {
|
|
404
|
+
/** Include related records (foreign key relations) */
|
|
405
|
+
include?: ${modelName}IncludeInput;
|
|
406
|
+
};
|
|
369
407
|
|
|
370
408
|
/**
|
|
371
409
|
* The complete API for interacting with ${modelName} records.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suparisma",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma.",
|
|
3
|
+
"version": "1.2.3",
|
|
4
|
+
"description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma. Works with Next.js, Remix, React Native, and Expo.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"react",
|
|
23
|
+
"react-native",
|
|
24
|
+
"expo",
|
|
23
25
|
"hooks",
|
|
24
26
|
"supabase",
|
|
25
27
|
"prisma",
|
|
26
28
|
"typescript",
|
|
27
29
|
"nextjs",
|
|
28
|
-
"realtime"
|
|
30
|
+
"realtime",
|
|
31
|
+
"mobile"
|
|
29
32
|
],
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@supabase/supabase-js": "^2.49.4",
|
|
@@ -41,5 +44,21 @@
|
|
|
41
44
|
"@types/react": "^19.1.4",
|
|
42
45
|
"prisma": "^6.8.2",
|
|
43
46
|
"supabase-js": "link:@types/@supabase/supabase-js"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@react-native-async-storage/async-storage": ">=1.19.0",
|
|
50
|
+
"react-native-get-random-values": ">=1.9.0",
|
|
51
|
+
"react-native-url-polyfill": ">=2.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@react-native-async-storage/async-storage": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"react-native-get-random-values": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"react-native-url-polyfill": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
44
63
|
}
|
|
45
64
|
}
|