suparisma 0.0.3 → 1.0.0
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 +708 -72
- package/dist/generators/coreGenerator.js +1 -1
- package/dist/index.js +0 -0
- package/generated/hooks/useSuparismaAuditLog.ts +80 -0
- package/generated/hooks/useSuparismaThing.ts +82 -0
- package/generated/index.ts +55 -0
- package/generated/types/AuditLogTypes.ts +391 -0
- package/generated/types/ThingTypes.ts +394 -0
- package/generated/utils/core.ts +1490 -0
- package/generated/utils/supabase-client.ts +10 -0
- package/package.json +8 -1
- package/dist/generated/supabase-client-generated.js +0 -7
- package/dist/hooks/generated/UserTypes.js +0 -2
- package/dist/hooks/generated/core.js +0 -1089
- package/dist/hooks/generated/index.js +0 -33
- package/dist/hooks/generated/useSuparismaUser.js +0 -60
- package/dist/suparisma/generated/hooks/useSuparismaUser.js +0 -61
- package/dist/suparisma/generated/index.js +0 -33
- package/dist/suparisma/generated/types/UserTypes.js +0 -4
- package/dist/suparisma/generated/utils/core.js +0 -1090
- package/dist/suparisma/generated/utils/supabase-client.js +0 -8
package/README.md
CHANGED
|
@@ -1,49 +1,120 @@
|
|
|
1
|
-
# Suparisma
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
1
|
+
# Suparisma
|
|
2
|
+
Supabase + Prisma!
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
A powerful, typesafe React hook generator for Supabase, driven by your Prisma schema. Suparisma provides you with real-time enabled CRUD hooks to interact with your Supabase database without writing any boilerplate code.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/suparisma)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Why Suparisma?](#why-suparisma)
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Detailed Usage](#detailed-usage)
|
|
19
|
+
- [Basic CRUD Operations](#basic-crud-operations)
|
|
20
|
+
- [Realtime Updates](#realtime-updates)
|
|
21
|
+
- [Filtering Data](#filtering-data)
|
|
22
|
+
- [Sorting Data](#sorting-data)
|
|
23
|
+
- [Pagination](#pagination)
|
|
24
|
+
- [Search Functionality](#search-functionality)
|
|
25
|
+
- [Schema Annotations](#schema-annotations)
|
|
26
|
+
- [Building UI Components](#building-ui-components)
|
|
27
|
+
- [Table with Filtering, Sorting, and Pagination](#table-with-filtering-sorting-and-pagination)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [Environment Variables](#environment-variables)
|
|
30
|
+
- [CLI Commands](#cli-commands)
|
|
31
|
+
- [Advanced Usage](#advanced-usage)
|
|
32
|
+
- [Custom Hooks](#custom-hooks)
|
|
33
|
+
- [Error Handling](#error-handling)
|
|
34
|
+
- [Performance Optimization](#performance-optimization)
|
|
35
|
+
- [API Reference](#api-reference)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Why Suparisma?
|
|
41
|
+
|
|
42
|
+
I love Supabase and Prisma and use them extensively in my projects, but combining them effectively for frontend development has always been frustrating. The typical solutions involve either setting up tRPC with constant refetching or implementing complex websocket solutions that often feel like overkill.
|
|
43
|
+
|
|
44
|
+
While Prisma gives me perfect type safety on the server, and Supabase offers great realtime capabilities, bridging these two worlds has remained a pain point. I've struggled to maintain type safety across the stack while efficiently leveraging Supabase's realtime features without resorting to excessive API calls or overly complex architectures.
|
|
45
|
+
|
|
46
|
+
Suparisma bridges this gap by:
|
|
47
|
+
|
|
48
|
+
- Creating **typesafe CRUD hooks** for all your Supabase tables
|
|
49
|
+
- Enabling easy **pagination, filtering, and search** on your data
|
|
50
|
+
- Leveraging both **Prisma** and **Supabase** official SDKs
|
|
51
|
+
- Respecting **Supabase's auth rules** for secure database access
|
|
52
|
+
- Working seamlessly with any React environment (Next.js, Remix, Tanstack Router, etc.)
|
|
18
53
|
|
|
19
54
|
## Features
|
|
20
55
|
|
|
21
56
|
- 🚀 **Auto-generated React hooks** based on your Prisma schema
|
|
22
|
-
- 🔄 **Real-time updates by default** for all tables (opt-out
|
|
57
|
+
- 🔄 **Real-time updates by default** for all tables (with opt-out capability)
|
|
23
58
|
- 🔒 **Type-safe interfaces** for all database operations
|
|
24
|
-
- 🔍 **Full-text search**
|
|
25
|
-
-
|
|
59
|
+
- 🔍 **Full-text search** with configurable annotations
|
|
60
|
+
- 🔢 **Pagination and sorting** built into every hook
|
|
61
|
+
- 🧩 **Prisma-like API** that feels familiar if you already use Prisma
|
|
62
|
+
- 📱 **Works with any React framework** including Next.js, Remix, etc.
|
|
63
|
+
- 🛠️ **Simple CLI** to generate hooks with a single command
|
|
26
64
|
|
|
27
65
|
## Installation
|
|
28
66
|
|
|
29
67
|
```bash
|
|
68
|
+
# Using npm
|
|
30
69
|
npm install suparisma
|
|
31
|
-
|
|
70
|
+
|
|
71
|
+
# Using yarn
|
|
32
72
|
yarn add suparisma
|
|
33
|
-
|
|
73
|
+
|
|
74
|
+
# Using pnpm
|
|
34
75
|
pnpm install suparisma
|
|
35
76
|
```
|
|
36
77
|
|
|
37
78
|
## Quick Start
|
|
38
79
|
|
|
39
|
-
1. **Add a Prisma schema**:
|
|
80
|
+
1. **Add a Prisma schema**: Ensure you have a valid `prisma/schema.prisma` file in your project
|
|
81
|
+
|
|
82
|
+
```prisma
|
|
83
|
+
// This is a sample Prisma schema file
|
|
84
|
+
generator client {
|
|
85
|
+
provider = "prisma-client-js"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
datasource db {
|
|
89
|
+
provider = "postgresql"
|
|
90
|
+
url = env("DATABASE_URL")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Realtime is enabled by default
|
|
94
|
+
model Thing {
|
|
95
|
+
id String @id @default(uuid())
|
|
96
|
+
name String? // @enableSearch
|
|
97
|
+
someNumber Int @default(0)
|
|
98
|
+
someEnum String @default("ONE")
|
|
99
|
+
createdAt DateTime @default(now())
|
|
100
|
+
updatedAt DateTime @updatedAt
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// @disableRealtime - Opt out of realtime for this model
|
|
104
|
+
model AuditLog {
|
|
105
|
+
id String @id @default(uuid())
|
|
106
|
+
action String
|
|
107
|
+
details String?
|
|
108
|
+
createdAt DateTime @default(now())
|
|
109
|
+
}
|
|
110
|
+
```
|
|
40
111
|
|
|
41
112
|
2. **Set up required environment variables** in a `.env` file:
|
|
42
113
|
|
|
43
114
|
```
|
|
44
115
|
# Required for Prisma and Supabase
|
|
45
116
|
DATABASE_URL="postgresql://user:password@host:port/database"
|
|
46
|
-
DIRECT_URL="postgresql://user:password@host:port/database"
|
|
117
|
+
DIRECT_URL="postgresql://user:password@host:port/database"
|
|
47
118
|
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
48
119
|
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
|
|
49
120
|
```
|
|
@@ -53,75 +124,475 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
|
|
|
53
124
|
```bash
|
|
54
125
|
npx suparisma generate
|
|
55
126
|
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
Note: you can adjust the prisma schema path and the generated files output path with these ENV variables:
|
|
128
|
+
```bash
|
|
129
|
+
SUPARISMA_PRISMA_SCHEMA_PATH="./prisma/schema.prisma"
|
|
130
|
+
SUPARISMA_OUTPUT_DIR="./src/suparisma/generated"
|
|
131
|
+
```
|
|
132
|
+
Also make sure to not change any of these generated files directly as **they will always be overwritten**
|
|
61
133
|
|
|
62
134
|
4. **Use the hooks** in your React components:
|
|
63
135
|
|
|
64
136
|
```tsx
|
|
65
137
|
import useSuparisma from './src/suparisma/generated';
|
|
66
138
|
|
|
67
|
-
function
|
|
68
|
-
const
|
|
139
|
+
function ThingList() {
|
|
140
|
+
const {
|
|
141
|
+
data: things,
|
|
142
|
+
loading,
|
|
143
|
+
error,
|
|
144
|
+
create: createThing
|
|
145
|
+
} = useSuparisma.thing();
|
|
69
146
|
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
147
|
+
if (loading) return <div>Loading...</div>;
|
|
148
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
72
149
|
|
|
73
150
|
return (
|
|
74
151
|
<div>
|
|
75
|
-
<h1>
|
|
152
|
+
<h1>Things</h1>
|
|
76
153
|
<ul>
|
|
77
|
-
{
|
|
78
|
-
<li key={
|
|
154
|
+
{things?.map(thing => (
|
|
155
|
+
<li key={thing.id}>{thing.name} (Number: {thing.someNumber})</li>
|
|
79
156
|
))}
|
|
80
157
|
</ul>
|
|
81
158
|
|
|
82
|
-
<button onClick={() =>
|
|
83
|
-
|
|
159
|
+
<button onClick={() => createThing({
|
|
160
|
+
name: "New Thing",
|
|
161
|
+
someNumber: Math.floor(Math.random() * 100)
|
|
162
|
+
})}>
|
|
163
|
+
Add Thing
|
|
84
164
|
</button>
|
|
85
165
|
</div>
|
|
86
166
|
);
|
|
87
167
|
}
|
|
88
168
|
```
|
|
89
169
|
|
|
90
|
-
##
|
|
170
|
+
## Detailed Usage
|
|
171
|
+
|
|
172
|
+
### Basic CRUD Operations
|
|
173
|
+
|
|
174
|
+
Every generated hook provides a complete set of CRUD operations:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const {
|
|
178
|
+
// State
|
|
179
|
+
data, // Array of records
|
|
180
|
+
loading, // Boolean loading state
|
|
181
|
+
error, // Error object if any
|
|
182
|
+
|
|
183
|
+
// Actions
|
|
184
|
+
create, // Create a new record
|
|
185
|
+
update, // Update existing record(s)
|
|
186
|
+
delete, // Delete a record
|
|
187
|
+
upsert, // Create or update a record
|
|
188
|
+
|
|
189
|
+
// Helpers
|
|
190
|
+
count, // Get count of records (respects where filters)
|
|
191
|
+
refresh, // Manually refresh data
|
|
192
|
+
} = useSuparisma.modelName();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Creating Records
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
const { create: createThing } = useSuparisma.thing();
|
|
199
|
+
|
|
200
|
+
// Create a single record
|
|
201
|
+
await createThing({ name: "Cool Thing", someNumber: 42 });
|
|
202
|
+
|
|
203
|
+
// Create with nested data if your schema supports it
|
|
204
|
+
await createThing({
|
|
205
|
+
name: "Cool Thing",
|
|
206
|
+
someNumber: 42,
|
|
207
|
+
// If you had relations defined:
|
|
208
|
+
// tags: {
|
|
209
|
+
// create: [
|
|
210
|
+
// { name: "Important" },
|
|
211
|
+
// { name: "Featured" }
|
|
212
|
+
// ]
|
|
213
|
+
// }
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Reading Records
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// Get all records with default pagination (first 10)
|
|
221
|
+
const { data: things } = useSuparisma.thing();
|
|
222
|
+
|
|
223
|
+
// With filtering
|
|
224
|
+
const { data: importantThings } = useSuparisma.thing({
|
|
225
|
+
where: { someEnum: "ONE" }
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// With custom pagination
|
|
229
|
+
const { data: recentThings } = useSuparisma.thing({
|
|
230
|
+
orderBy: { createdAt: "desc" },
|
|
231
|
+
limit: 5
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Updating Records
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
const { update: updateThing } = useSuparisma.thing();
|
|
239
|
+
|
|
240
|
+
// Update by ID
|
|
241
|
+
await updateThing({
|
|
242
|
+
where: { id: "thing-id-123" },
|
|
243
|
+
data: { name: "Updated Thing" }
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Update many records matching a filter
|
|
247
|
+
await updateThing({
|
|
248
|
+
where: { someEnum: "ONE" },
|
|
249
|
+
data: { someEnum: "TWO" }
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Deleting Records
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
const { delete: deleteThing } = useSuparisma.thing();
|
|
257
|
+
|
|
258
|
+
// Delete by ID
|
|
259
|
+
await deleteThing({ id: "thing-id-123" });
|
|
260
|
+
|
|
261
|
+
// Delete with more complex filter
|
|
262
|
+
await deleteThing({ someNumber: 0 });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Realtime Updates
|
|
266
|
+
|
|
267
|
+
Realtime updates are enabled by default for all models. The `data` will automatically update when changes occur in the database.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Enable realtime (default)
|
|
271
|
+
const { data: things } = useSuparisma.thing({ realtime: true });
|
|
272
|
+
|
|
273
|
+
// Disable realtime for this particular hook instance
|
|
274
|
+
const { data: logsNoRealtime } = useSuparisma.auditLog({ realtime: false });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Filtering Data
|
|
278
|
+
|
|
279
|
+
Filter data using Prisma-like syntax:
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// Basic equality
|
|
283
|
+
const { data } = useSuparisma.thing({
|
|
284
|
+
where: { someEnum: "ONE" }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Multiple conditions (AND)
|
|
288
|
+
const { data } = useSuparisma.thing({
|
|
289
|
+
where: {
|
|
290
|
+
someEnum: "ONE",
|
|
291
|
+
someNumber: { gt: 10 }
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Using operators
|
|
296
|
+
const { data } = useSuparisma.thing({
|
|
297
|
+
where: {
|
|
298
|
+
createdAt: { gte: new Date('2023-01-01') },
|
|
299
|
+
name: { contains: "cool" }
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Sorting Data
|
|
305
|
+
|
|
306
|
+
Sort data using Prisma-like ordering:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// Single field sort
|
|
310
|
+
const { data } = useSuparisma.thing({
|
|
311
|
+
orderBy: { createdAt: "desc" }
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Multiple field sort
|
|
315
|
+
const { data } = useSuparisma.thing({
|
|
316
|
+
orderBy: [
|
|
317
|
+
{ someEnum: "asc" },
|
|
318
|
+
{ createdAt: "desc" }
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Pagination
|
|
324
|
+
|
|
325
|
+
Suparisma supports offset-based pagination:
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
// Offset-based pagination (page 1, 10 items per page)
|
|
329
|
+
const { data } = useSuparisma.thing({
|
|
330
|
+
offset: 0,
|
|
331
|
+
limit: 10
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Next page
|
|
335
|
+
const { data: page2 } = useSuparisma.thing({
|
|
336
|
+
offset: 10,
|
|
337
|
+
limit: 10
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Get total count
|
|
341
|
+
const { data, count } = useSuparisma.thing();
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Search Functionality
|
|
345
|
+
|
|
346
|
+
For fields annotated with `// @enableSearch`, you can use full-text search:
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
// Search things by name
|
|
350
|
+
const { data: searchResults } = useSuparisma.thing({
|
|
351
|
+
search: {
|
|
352
|
+
query: "cool",
|
|
353
|
+
fields: ["name"]
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Schema Annotations
|
|
91
359
|
|
|
92
|
-
|
|
360
|
+
Suparisma uses comments in your Prisma schema to configure behavior:
|
|
93
361
|
|
|
94
362
|
```prisma
|
|
95
|
-
//
|
|
96
|
-
model User {
|
|
97
|
-
id String @id @default(uuid())
|
|
98
|
-
email String @unique
|
|
99
|
-
name String? // @enableSearch
|
|
100
|
-
createdAt DateTime @default(now())
|
|
101
|
-
updatedAt DateTime @updatedAt
|
|
102
|
-
}
|
|
363
|
+
// Model level annotations
|
|
103
364
|
|
|
104
365
|
// @disableRealtime - Opt out of realtime for this model
|
|
105
366
|
model AuditLog {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
367
|
+
// ...fields
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Field level annotations
|
|
371
|
+
|
|
372
|
+
model Thing {
|
|
373
|
+
id String @id @default(uuid())
|
|
374
|
+
name String? // @enableSearch - Enable full-text search for this field
|
|
375
|
+
description String? // @enableSearch - Can add to multiple fields
|
|
376
|
+
someNumber Int
|
|
110
377
|
}
|
|
111
378
|
```
|
|
112
379
|
|
|
113
|
-
|
|
380
|
+
Available annotations:
|
|
114
381
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
382
|
+
| Annotation | Description | Location |
|
|
383
|
+
|------------|-------------|----------|
|
|
384
|
+
| `@disableRealtime` | Disables real-time updates for this model | Model (before definition) |
|
|
385
|
+
| `@enableSearch` | Enables full-text search on this field | Field (after definition) |
|
|
119
386
|
|
|
120
|
-
|
|
387
|
+
## Building UI Components
|
|
121
388
|
|
|
122
|
-
|
|
389
|
+
### Table with Filtering, Sorting, and Pagination
|
|
123
390
|
|
|
124
|
-
|
|
391
|
+
Here's a complete example of a data table with filtering, sorting, and pagination:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
import { useState } from "react";
|
|
395
|
+
import useSuparisma from '../generated';
|
|
396
|
+
|
|
397
|
+
export default function ThingTable() {
|
|
398
|
+
const itemsPerPage = 10;
|
|
399
|
+
const [page, setPage] = useState(0);
|
|
400
|
+
const [enumFilter, setEnumFilter] = useState("");
|
|
401
|
+
const [sortField, setSortField] = useState("updatedAt");
|
|
402
|
+
const [sortDirection, setSortDirection] = useState("desc");
|
|
403
|
+
|
|
404
|
+
const {
|
|
405
|
+
data: things,
|
|
406
|
+
loading: isLoading,
|
|
407
|
+
error,
|
|
408
|
+
create: createThing,
|
|
409
|
+
update: updateThing,
|
|
410
|
+
delete: deleteThing,
|
|
411
|
+
count: thingCount,
|
|
412
|
+
} = useSuparisma.thing({
|
|
413
|
+
realtime: true,
|
|
414
|
+
limit: itemsPerPage,
|
|
415
|
+
offset: page * itemsPerPage,
|
|
416
|
+
where: enumFilter ? {
|
|
417
|
+
someEnum: enumFilter
|
|
418
|
+
} : undefined,
|
|
419
|
+
orderBy: {
|
|
420
|
+
[sortField]: sortDirection
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if(error) {
|
|
425
|
+
return <div>Error: {error.message}</div>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className="container mx-auto p-4">
|
|
430
|
+
<div className="flex justify-between items-center mb-4">
|
|
431
|
+
<h1 className="text-2xl font-bold">Thing List</h1>
|
|
432
|
+
<button
|
|
433
|
+
onClick={() => createThing({
|
|
434
|
+
name: 'New Thing',
|
|
435
|
+
someNumber: Math.floor(Math.random() * 100)
|
|
436
|
+
})}
|
|
437
|
+
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
438
|
+
>
|
|
439
|
+
Create New Thing
|
|
440
|
+
</button>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
{/* Filter and Sort Controls */}
|
|
444
|
+
<div className="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
445
|
+
<div>
|
|
446
|
+
<label htmlFor="enumFilter" className="block text-sm font-medium mb-1">
|
|
447
|
+
Filter by Enum
|
|
448
|
+
</label>
|
|
449
|
+
<select
|
|
450
|
+
value={enumFilter}
|
|
451
|
+
onChange={(e) => {
|
|
452
|
+
setEnumFilter(e.target.value);
|
|
453
|
+
setPage(0); // Reset to first page when filter changes
|
|
454
|
+
}}
|
|
455
|
+
className="w-full p-2 border rounded-md"
|
|
456
|
+
>
|
|
457
|
+
<option value="">All</option>
|
|
458
|
+
<option value="ONE">ONE</option>
|
|
459
|
+
<option value="TWO">TWO</option>
|
|
460
|
+
<option value="THREE">THREE</option>
|
|
461
|
+
</select>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div>
|
|
465
|
+
<label htmlFor="sortField" className="block text-sm font-medium mb-1">
|
|
466
|
+
Sort By
|
|
467
|
+
</label>
|
|
468
|
+
<select
|
|
469
|
+
value={sortField}
|
|
470
|
+
onChange={(e) => {
|
|
471
|
+
setSortField(e.target.value);
|
|
472
|
+
setPage(0);
|
|
473
|
+
}}
|
|
474
|
+
className="w-full p-2 border rounded-md"
|
|
475
|
+
>
|
|
476
|
+
<option value="createdAt">Created Date</option>
|
|
477
|
+
<option value="updatedAt">Updated Date</option>
|
|
478
|
+
<option value="someNumber">Number</option>
|
|
479
|
+
</select>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<div>
|
|
483
|
+
<label htmlFor="sortDirection" className="block text-sm font-medium mb-1">
|
|
484
|
+
Direction
|
|
485
|
+
</label>
|
|
486
|
+
<select
|
|
487
|
+
value={sortDirection}
|
|
488
|
+
onChange={(e) => {
|
|
489
|
+
setSortDirection(e.target.value);
|
|
490
|
+
setPage(0);
|
|
491
|
+
}}
|
|
492
|
+
className="w-full p-2 border rounded-md"
|
|
493
|
+
>
|
|
494
|
+
<option value="asc">Ascending</option>
|
|
495
|
+
<option value="desc">Descending</option>
|
|
496
|
+
</select>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
{/* Data Table */}
|
|
501
|
+
<div className="overflow-x-auto">
|
|
502
|
+
<table className="min-w-full bg-white border">
|
|
503
|
+
<thead>
|
|
504
|
+
<tr className="bg-gray-100">
|
|
505
|
+
<th className="py-2 px-4 border-b text-left">Name</th>
|
|
506
|
+
<th className="py-2 px-4 border-b text-left">Number</th>
|
|
507
|
+
<th className="py-2 px-4 border-b text-left">Enum</th>
|
|
508
|
+
<th className="py-2 px-4 border-b text-left">ID</th>
|
|
509
|
+
<th className="py-2 px-4 border-b text-left">Actions</th>
|
|
510
|
+
</tr>
|
|
511
|
+
</thead>
|
|
512
|
+
<tbody>
|
|
513
|
+
{isLoading ? (
|
|
514
|
+
<tr>
|
|
515
|
+
<td colSpan={5} className="py-4 text-center">Loading...</td>
|
|
516
|
+
</tr>
|
|
517
|
+
) : things?.length === 0 ? (
|
|
518
|
+
<tr>
|
|
519
|
+
<td colSpan={5} className="py-4 text-center text-gray-500">
|
|
520
|
+
No records found
|
|
521
|
+
</td>
|
|
522
|
+
</tr>
|
|
523
|
+
) : (
|
|
524
|
+
things?.map((thing) => (
|
|
525
|
+
<tr key={thing.id} className="hover:bg-gray-50">
|
|
526
|
+
<td className="py-2 px-4 border-b">{thing.name}</td>
|
|
527
|
+
<td className="py-2 px-4 border-b">{thing.someNumber}</td>
|
|
528
|
+
<td className="py-2 px-4 border-b">{thing.someEnum}</td>
|
|
529
|
+
<td className="py-2 px-4 border-b">{thing.id}</td>
|
|
530
|
+
<td className="py-2 px-4 border-b">
|
|
531
|
+
<button
|
|
532
|
+
onClick={() => updateThing({
|
|
533
|
+
where: { id: thing.id },
|
|
534
|
+
data: { name: `Updated ${thing.name}` }
|
|
535
|
+
})}
|
|
536
|
+
className="bg-green-500 hover:bg-green-700 text-white py-1 px-2 rounded mr-2 text-sm"
|
|
537
|
+
>
|
|
538
|
+
Update
|
|
539
|
+
</button>
|
|
540
|
+
<button
|
|
541
|
+
onClick={() => deleteThing({ id: thing.id })}
|
|
542
|
+
className="bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded text-sm"
|
|
543
|
+
>
|
|
544
|
+
Delete
|
|
545
|
+
</button>
|
|
546
|
+
</td>
|
|
547
|
+
</tr>
|
|
548
|
+
))
|
|
549
|
+
)}
|
|
550
|
+
</tbody>
|
|
551
|
+
</table>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{/* Pagination Controls */}
|
|
555
|
+
<div className="mt-4 flex justify-center items-center">
|
|
556
|
+
<button
|
|
557
|
+
onClick={() => setPage(prev => Math.max(0, prev - 1))}
|
|
558
|
+
disabled={page === 0}
|
|
559
|
+
className="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-l disabled:opacity-50"
|
|
560
|
+
>
|
|
561
|
+
Previous
|
|
562
|
+
</button>
|
|
563
|
+
<span className="py-2 px-4">
|
|
564
|
+
Page {page + 1}
|
|
565
|
+
{thingCount && ` of ${Math.ceil(Number(thingCount) / itemsPerPage)}`}
|
|
566
|
+
</span>
|
|
567
|
+
<button
|
|
568
|
+
onClick={() => setPage(prev => prev + 1)}
|
|
569
|
+
disabled={!things || things.length < itemsPerPage}
|
|
570
|
+
className="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-r disabled:opacity-50"
|
|
571
|
+
>
|
|
572
|
+
Next
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Configuration
|
|
581
|
+
|
|
582
|
+
### Environment Variables
|
|
583
|
+
|
|
584
|
+
| Variable | Required | Description | Example |
|
|
585
|
+
|----------|----------|-------------|---------|
|
|
586
|
+
| `DATABASE_URL` | Yes | Postgres database URL used by Prisma | `postgresql://user:pass@host:port/db` |
|
|
587
|
+
| `DIRECT_URL` | Yes | Direct URL to Postgres DB for realtime setup | `postgresql://user:pass@host:port/db` |
|
|
588
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Yes | Your Supabase project URL | `https://xyz.supabase.co` |
|
|
589
|
+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes | Supabase anonymous key | `eyJh...` |
|
|
590
|
+
| `SUPARISMA_OUTPUT_DIR` | No | Custom output directory | `src/lib/suparisma` |
|
|
591
|
+
| `SUPARISMA_PRISMA_SCHEMA_PATH` | No | Custom schema path | `db/schema.prisma` |
|
|
592
|
+
|
|
593
|
+
### CLI Commands
|
|
594
|
+
|
|
595
|
+
Suparisma provides a simple command-line interface:
|
|
125
596
|
|
|
126
597
|
```bash
|
|
127
598
|
# Generate hooks based on your Prisma schema
|
|
@@ -131,26 +602,191 @@ npx suparisma generate
|
|
|
131
602
|
npx suparisma help
|
|
132
603
|
```
|
|
133
604
|
|
|
134
|
-
##
|
|
605
|
+
## Stale Models Cleanup
|
|
606
|
+
|
|
607
|
+
When you delete a model from your Prisma schema and run the generation command, Suparisma automatically:
|
|
608
|
+
- Detects changes to your schema
|
|
609
|
+
- Deletes the entire generated directory
|
|
610
|
+
- Regenerates all hooks and types based on your current schema
|
|
611
|
+
|
|
612
|
+
This ensures you never have stale files lingering around for models that no longer exist in your schema.
|
|
613
|
+
|
|
614
|
+
## Advanced Usage
|
|
135
615
|
|
|
136
|
-
|
|
616
|
+
### Custom Hooks
|
|
137
617
|
|
|
618
|
+
You can combine Suparisma hooks with your own custom hooks for advanced use cases:
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
import { useState } from 'react';
|
|
622
|
+
import useSuparisma from '../generated';
|
|
623
|
+
|
|
624
|
+
// Custom hook for managing important things
|
|
625
|
+
function useImportantThings() {
|
|
626
|
+
const [category, setCategory] = useState<string>("ONE");
|
|
627
|
+
|
|
628
|
+
const {
|
|
629
|
+
data: things,
|
|
630
|
+
loading,
|
|
631
|
+
error,
|
|
632
|
+
create: createThing,
|
|
633
|
+
update: updateThing,
|
|
634
|
+
} = useSuparisma.thing({
|
|
635
|
+
where: { someEnum: category },
|
|
636
|
+
orderBy: { someNumber: "desc" }
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const addThing = (name: string, number: number) => {
|
|
640
|
+
return createThing({
|
|
641
|
+
name,
|
|
642
|
+
someNumber: number,
|
|
643
|
+
someEnum: category
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const changeCategory = (newCategory: string) => {
|
|
648
|
+
setCategory(newCategory);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
things,
|
|
653
|
+
loading,
|
|
654
|
+
error,
|
|
655
|
+
addThing,
|
|
656
|
+
updateThing,
|
|
657
|
+
currentCategory: category,
|
|
658
|
+
changeCategory
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Error Handling
|
|
664
|
+
|
|
665
|
+
Handle errors gracefully with try/catch blocks:
|
|
666
|
+
|
|
667
|
+
```tsx
|
|
668
|
+
const { create, error } = useSuparisma.thing();
|
|
669
|
+
|
|
670
|
+
async function handleSubmit(event) {
|
|
671
|
+
event.preventDefault();
|
|
672
|
+
try {
|
|
673
|
+
const result = await create({
|
|
674
|
+
name: formData.name,
|
|
675
|
+
someNumber: parseInt(formData.number)
|
|
676
|
+
});
|
|
677
|
+
console.log('Created!', result);
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.error('Failed to create thing:', err);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Performance Optimization
|
|
685
|
+
|
|
686
|
+
Optimize performance by:
|
|
687
|
+
|
|
688
|
+
1. Disabling realtime when not needed
|
|
689
|
+
2. Using pagination to limit data size
|
|
690
|
+
3. Using precise filter conditions
|
|
691
|
+
|
|
692
|
+
```tsx
|
|
693
|
+
// Only get what you need
|
|
694
|
+
const { data } = useSuparisma.thing({
|
|
695
|
+
realtime: false, // Disable realtime if not needed
|
|
696
|
+
where: { someEnum: "ONE" }, // Only get specific items
|
|
697
|
+
select: { id: true, name: true, someNumber: true }, // Only select needed fields
|
|
698
|
+
limit: 10 // Limit results
|
|
699
|
+
});
|
|
138
700
|
```
|
|
139
|
-
# Optional: Customize output directory
|
|
140
|
-
SUPARISMA_OUTPUT_DIR="src/hooks/generated"
|
|
141
701
|
|
|
142
|
-
|
|
143
|
-
|
|
702
|
+
## API Reference
|
|
703
|
+
|
|
704
|
+
### Hook Options
|
|
705
|
+
|
|
706
|
+
| Option | Type | Description |
|
|
707
|
+
|--------|------|-------------|
|
|
708
|
+
| `where` | `object` | Filter conditions for the query |
|
|
709
|
+
| `orderBy` | `object \| array` | Sorting options |
|
|
710
|
+
| `limit` | `number` | Maximum number of records to return |
|
|
711
|
+
| `offset` | `number` | Number of records to skip for pagination |
|
|
712
|
+
| `realtime` | `boolean` | Enable/disable real-time updates |
|
|
713
|
+
| `select` | `object` | Fields to include in the response |
|
|
714
|
+
| `include` | `object` | Related records to include |
|
|
715
|
+
| `search` | `object` | Full-text search configuration |
|
|
716
|
+
|
|
717
|
+
### Hook Return Value
|
|
718
|
+
|
|
719
|
+
| Property | Type | Description |
|
|
720
|
+
|----------|------|-------------|
|
|
721
|
+
| `data` | `array` | Array of records matching the query |
|
|
722
|
+
| `loading` | `boolean` | Loading state |
|
|
723
|
+
| `error` | `Error \| null` | Error object if request failed |
|
|
724
|
+
| `create` | `function` | Create a new record |
|
|
725
|
+
| `update` | `function` | Update existing record(s) |
|
|
726
|
+
| `delete` | `function` | Delete a record |
|
|
727
|
+
| `upsert` | `function` | Create or update a record |
|
|
728
|
+
| `count` | `function` | Get count of records |
|
|
729
|
+
| `refresh` | `function` | Manually refresh data |
|
|
730
|
+
|
|
731
|
+
## Troubleshooting
|
|
732
|
+
|
|
733
|
+
### Common Issues
|
|
734
|
+
|
|
735
|
+
**Realtime not working**
|
|
736
|
+
- First, make sure you have ran npx suparisma generate as that will automatically add all your tables to the realtime supabase publication.
|
|
737
|
+
- Second, Make sure to have realtime: true in the hook usage and also in supabase go to tables > publications > supabase_realtime and there you must find the tables you created in prisma in there or the realtime is not working properly.
|
|
738
|
+
|
|
739
|
+
**No permissions/this table doesn't exist**
|
|
740
|
+
- If you ever run into such issue make sure the **anon** has suffiecient permissions to access your tables, this issue especially is common when you do prisma migrate.
|
|
741
|
+
```sql
|
|
742
|
+
-- Replace YOUR_TABLE_NAME with the actual table names affected by your migration
|
|
743
|
+
|
|
744
|
+
-- Grant usage on the schema to anon
|
|
745
|
+
GRANT USAGE ON SCHEMA public TO anon;
|
|
746
|
+
|
|
747
|
+
-- Grant SELECT, INSERT, UPDATE, DELETE on tables to anon
|
|
748
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO anon;
|
|
749
|
+
|
|
750
|
+
-- Ensure future tables also grant these permissions
|
|
751
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
752
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO anon;
|
|
753
|
+
|
|
754
|
+
-- Grant usage on sequences to anon (if using auto-increment IDs)
|
|
755
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon;
|
|
756
|
+
|
|
757
|
+
-- Ensure future sequences also grant usage to anon
|
|
758
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
759
|
+
GRANT USAGE, SELECT ON SEQUENCES TO anon;
|
|
144
760
|
```
|
|
145
761
|
|
|
146
|
-
|
|
762
|
+
**"Unknown command: undefined"**
|
|
763
|
+
|
|
764
|
+
This happens when running the CLI without specifying a command. Use `npx suparisma generate` instead.
|
|
765
|
+
|
|
766
|
+
**"DIRECT_URL environment variable not found"**
|
|
767
|
+
|
|
768
|
+
You need to provide a direct PostgreSQL connection URL in your `.env` file for realtime functionality.
|
|
769
|
+
|
|
770
|
+
**"Table X was already in supabase_realtime publication"**
|
|
771
|
+
|
|
772
|
+
This is just an informational message, not an error. Your table is already configured for realtime updates.
|
|
773
|
+
|
|
774
|
+
**Hook data doesn't update in real-time**
|
|
775
|
+
|
|
776
|
+
Check:
|
|
777
|
+
1. The model doesn't have `@disableRealtime` annotation
|
|
778
|
+
2. The hook is called with `realtime: true` (default)
|
|
779
|
+
3. Your Supabase project has realtime enabled in the dashboard
|
|
780
|
+
|
|
781
|
+
## Contributing
|
|
147
782
|
|
|
148
|
-
|
|
783
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
149
784
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
785
|
+
1. Fork the repository
|
|
786
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
787
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
788
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
789
|
+
5. Open a Pull Request
|
|
154
790
|
|
|
155
791
|
## License
|
|
156
792
|
|