suparisma 0.0.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +707 -71
- package/dist/generators/coreGenerator.js +205 -43
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,49 +1,120 @@
|
|
|
1
1
|
# Suparisma
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
|
|
@@ -155,6 +155,37 @@ export type SearchState = {
|
|
|
155
155
|
clearQueries: () => void;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Compare two values for sorting with proper type handling
|
|
160
|
+
*/
|
|
161
|
+
function compareValues(a: any, b: any, direction: 'asc' | 'desc'): number {
|
|
162
|
+
// Handle undefined/null values
|
|
163
|
+
if (a === undefined || a === null) return direction === 'asc' ? -1 : 1;
|
|
164
|
+
if (b === undefined || b === null) return direction === 'asc' ? 1 : -1;
|
|
165
|
+
|
|
166
|
+
// Handle numbers properly to ensure numeric comparison
|
|
167
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
168
|
+
return direction === 'asc'
|
|
169
|
+
? a - b
|
|
170
|
+
: b - a;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle dates (convert to timestamps for comparison)
|
|
174
|
+
if (a instanceof Date && b instanceof Date) {
|
|
175
|
+
return direction === 'asc'
|
|
176
|
+
? a.getTime() - b.getTime()
|
|
177
|
+
: b.getTime() - a.getTime();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle strings or mixed types with string conversion
|
|
181
|
+
const aStr = String(a);
|
|
182
|
+
const bStr = String(b);
|
|
183
|
+
|
|
184
|
+
return direction === 'asc'
|
|
185
|
+
? aStr.localeCompare(bStr)
|
|
186
|
+
: bStr.localeCompare(aStr);
|
|
187
|
+
}
|
|
188
|
+
|
|
158
189
|
/**
|
|
159
190
|
* Convert a type-safe where filter to Supabase filter string
|
|
160
191
|
*/
|
|
@@ -316,11 +347,17 @@ export function applyOrderBy<T>(
|
|
|
316
347
|
|
|
317
348
|
// Apply each order by clause
|
|
318
349
|
let orderedQuery = query;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
350
|
+
|
|
351
|
+
// Handle orderBy as array or single object
|
|
352
|
+
const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
353
|
+
|
|
354
|
+
for (const orderByClause of orderByArray) {
|
|
355
|
+
for (const [key, direction] of Object.entries(orderByClause)) {
|
|
356
|
+
// @ts-ignore: Supabase typing issue
|
|
357
|
+
orderedQuery = orderedQuery.order(key, {
|
|
358
|
+
ascending: direction === 'asc'
|
|
359
|
+
});
|
|
360
|
+
}
|
|
324
361
|
}
|
|
325
362
|
|
|
326
363
|
return orderedQuery;
|
|
@@ -387,9 +424,29 @@ export function createSuparismaHook<
|
|
|
387
424
|
offset,
|
|
388
425
|
} = options;
|
|
389
426
|
|
|
390
|
-
//
|
|
391
|
-
const
|
|
392
|
-
|
|
427
|
+
// Refs to store the latest options for realtime handlers
|
|
428
|
+
const whereRef = useRef(where);
|
|
429
|
+
const orderByRef = useRef(orderBy);
|
|
430
|
+
const limitRef = useRef(limit);
|
|
431
|
+
const offsetRef = useRef(offset);
|
|
432
|
+
|
|
433
|
+
// Update refs whenever options change
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
whereRef.current = where;
|
|
436
|
+
}, [where]);
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
orderByRef.current = orderBy;
|
|
440
|
+
}, [orderBy]);
|
|
441
|
+
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
limitRef.current = limit;
|
|
444
|
+
}, [limit]);
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
offsetRef.current = offset;
|
|
448
|
+
}, [offset]);
|
|
449
|
+
|
|
393
450
|
// Single data collection for holding results
|
|
394
451
|
const [data, setData] = useState<TWithRelations[]>([]);
|
|
395
452
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -680,7 +737,7 @@ export function createSuparismaHook<
|
|
|
680
737
|
}
|
|
681
738
|
|
|
682
739
|
// Apply offset if provided (for pagination)
|
|
683
|
-
if (params?.skip) {
|
|
740
|
+
if (params?.skip !== undefined && params.skip >= 0) {
|
|
684
741
|
query = query.range(params.skip, params.skip + (params.take || 10) - 1);
|
|
685
742
|
}
|
|
686
743
|
|
|
@@ -773,14 +830,11 @@ export function createSuparismaHook<
|
|
|
773
830
|
|
|
774
831
|
const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
|
|
775
832
|
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
const currentOffset = offset;
|
|
782
|
-
|
|
783
|
-
console.log(\`Setting up subscription for \${tableName} with filter: \${currentFilter}\`);
|
|
833
|
+
// Use a dynamic filter string builder inside the event handler or rely on Supabase
|
|
834
|
+
// For the subscription filter, we must use the initial computedFilter or a stable one.
|
|
835
|
+
// However, for client-side logic (sorting, adding/removing from list), we use refs.
|
|
836
|
+
const initialComputedFilter = where ? buildFilterString(where) : realtimeFilter;
|
|
837
|
+
console.log(\`Setting up subscription for \${tableName} with initial filter: \${initialComputedFilter}\`);
|
|
784
838
|
|
|
785
839
|
const channel = supabase
|
|
786
840
|
.channel(channelId)
|
|
@@ -790,11 +844,17 @@ export function createSuparismaHook<
|
|
|
790
844
|
event: '*',
|
|
791
845
|
schema: 'public',
|
|
792
846
|
table: tableName,
|
|
793
|
-
filter:
|
|
847
|
+
filter: initialComputedFilter, // Subscription filter uses initial state
|
|
794
848
|
},
|
|
795
849
|
(payload) => {
|
|
796
850
|
console.log(\`Received \${payload.eventType} event for \${tableName}\`, payload);
|
|
797
851
|
|
|
852
|
+
// Access current options via refs inside the event handler
|
|
853
|
+
const currentWhere = whereRef.current;
|
|
854
|
+
const currentOrderBy = orderByRef.current;
|
|
855
|
+
const currentLimit = limitRef.current;
|
|
856
|
+
const currentOffset = offsetRef.current; // Not directly used in handlers but good for consistency
|
|
857
|
+
|
|
798
858
|
// Skip realtime updates when search is active
|
|
799
859
|
if (isSearchingRef.current) return;
|
|
800
860
|
|
|
@@ -806,7 +866,7 @@ export function createSuparismaHook<
|
|
|
806
866
|
console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
|
|
807
867
|
|
|
808
868
|
// Check if this record matches our filter if we have one
|
|
809
|
-
if (currentWhere) {
|
|
869
|
+
if (currentWhere) { // Use ref value
|
|
810
870
|
let matchesFilter = true;
|
|
811
871
|
|
|
812
872
|
// Check each filter condition
|
|
@@ -838,27 +898,43 @@ export function createSuparismaHook<
|
|
|
838
898
|
}
|
|
839
899
|
|
|
840
900
|
// Add the new record to the data
|
|
841
|
-
let newData = [
|
|
901
|
+
let newData = [...prev, newRecord]; // Changed: Use spread on prev for immutability
|
|
842
902
|
|
|
843
903
|
// Apply ordering if specified
|
|
844
|
-
if (currentOrderBy) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
904
|
+
if (currentOrderBy) { // Use ref value
|
|
905
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
906
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
907
|
+
? currentOrderBy
|
|
908
|
+
: [currentOrderBy];
|
|
909
|
+
|
|
910
|
+
// Apply each sort in sequence
|
|
911
|
+
newData = [...newData].sort((a, b) => {
|
|
912
|
+
// Check each orderBy clause in sequence
|
|
913
|
+
for (const orderByClause of orderByArray) {
|
|
914
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
915
|
+
const aValue = a[field as keyof typeof a];
|
|
916
|
+
const bValue = b[field as keyof typeof b];
|
|
917
|
+
|
|
918
|
+
// Skip if values are equal and move to next criterion
|
|
919
|
+
if (aValue === bValue) continue;
|
|
920
|
+
|
|
921
|
+
// Use the compareValues function for proper type handling
|
|
922
|
+
return compareValues(aValue, bValue, direction as 'asc' | 'desc');
|
|
855
923
|
}
|
|
856
|
-
}
|
|
857
|
-
|
|
924
|
+
}
|
|
925
|
+
return 0; // Equal if all criteria match
|
|
926
|
+
});
|
|
927
|
+
} else if (hasCreatedAt) {
|
|
928
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
929
|
+
newData = [...newData].sort((a, b) => {
|
|
930
|
+
const aValue = a[createdAtField as keyof typeof a];
|
|
931
|
+
const bValue = b[createdAtField as keyof typeof b];
|
|
932
|
+
return compareValues(aValue, bValue, 'desc');
|
|
933
|
+
});
|
|
858
934
|
}
|
|
859
935
|
|
|
860
936
|
// Apply limit if specified
|
|
861
|
-
if (currentLimit && currentLimit > 0) {
|
|
937
|
+
if (currentLimit && currentLimit > 0) { // Use ref value
|
|
862
938
|
newData = newData.slice(0, currentLimit);
|
|
863
939
|
}
|
|
864
940
|
|
|
@@ -874,6 +950,10 @@ export function createSuparismaHook<
|
|
|
874
950
|
} else if (payload.eventType === 'UPDATE') {
|
|
875
951
|
// Process update event
|
|
876
952
|
setData((prev) => {
|
|
953
|
+
// Access current options via refs
|
|
954
|
+
const currentOrderBy = orderByRef.current;
|
|
955
|
+
const currentLimit = limitRef.current; // If needed for re-fetch logic on update
|
|
956
|
+
|
|
877
957
|
// Skip if search is active
|
|
878
958
|
if (isSearchingRef.current) {
|
|
879
959
|
return prev;
|
|
@@ -886,15 +966,57 @@ export function createSuparismaHook<
|
|
|
886
966
|
: item
|
|
887
967
|
);
|
|
888
968
|
|
|
969
|
+
// Apply ordering again after update to ensure consistency
|
|
970
|
+
let sortedData = [...newData];
|
|
971
|
+
|
|
972
|
+
// Apply ordering if specified
|
|
973
|
+
if (currentOrderBy) { // Use ref value
|
|
974
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
975
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
976
|
+
? currentOrderBy
|
|
977
|
+
: [currentOrderBy];
|
|
978
|
+
|
|
979
|
+
// Apply each sort in sequence
|
|
980
|
+
sortedData = sortedData.sort((a, b) => {
|
|
981
|
+
// Check each orderBy clause in sequence
|
|
982
|
+
for (const orderByClause of orderByArray) {
|
|
983
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
984
|
+
const aValue = a[field as keyof typeof a];
|
|
985
|
+
const bValue = b[field as keyof typeof b];
|
|
986
|
+
|
|
987
|
+
// Skip if values are equal and move to next criterion
|
|
988
|
+
if (aValue === bValue) continue;
|
|
989
|
+
|
|
990
|
+
// Use the compareValues function for proper type handling
|
|
991
|
+
return compareValues(aValue, bValue, direction as 'asc' | 'desc');
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return 0; // Equal if all criteria match
|
|
995
|
+
});
|
|
996
|
+
} else if (hasCreatedAt) {
|
|
997
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
998
|
+
sortedData = sortedData.sort((a, b) => {
|
|
999
|
+
const aValue = a[createdAtField as keyof typeof a];
|
|
1000
|
+
const bValue = b[createdAtField as keyof typeof b];
|
|
1001
|
+
return compareValues(aValue, bValue, 'desc');
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
889
1005
|
// Fetch the updated count after the data changes
|
|
890
1006
|
// For updates, the count might not change but we fetch anyway to be consistent
|
|
891
1007
|
setTimeout(() => fetchTotalCount(), 0);
|
|
892
1008
|
|
|
893
|
-
return
|
|
1009
|
+
return sortedData;
|
|
894
1010
|
});
|
|
895
1011
|
} else if (payload.eventType === 'DELETE') {
|
|
896
1012
|
// Process delete event
|
|
897
1013
|
setData((prev) => {
|
|
1014
|
+
// Access current options via refs
|
|
1015
|
+
const currentWhere = whereRef.current;
|
|
1016
|
+
const currentOrderBy = orderByRef.current;
|
|
1017
|
+
const currentLimit = limitRef.current;
|
|
1018
|
+
const currentOffset = offsetRef.current;
|
|
1019
|
+
|
|
898
1020
|
// Skip if search is active
|
|
899
1021
|
if (isSearchingRef.current) {
|
|
900
1022
|
return prev;
|
|
@@ -913,21 +1035,61 @@ export function createSuparismaHook<
|
|
|
913
1035
|
setTimeout(() => fetchTotalCount(), 0);
|
|
914
1036
|
|
|
915
1037
|
// If we need to maintain the size with a limit
|
|
916
|
-
if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) {
|
|
1038
|
+
if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) { // Use ref value
|
|
917
1039
|
console.log(\`Record deleted with limit \${currentLimit}, will fetch additional record to maintain size\`);
|
|
918
1040
|
|
|
919
1041
|
// Use setTimeout to ensure this state update completes first
|
|
920
1042
|
setTimeout(() => {
|
|
921
1043
|
findMany({
|
|
922
|
-
where: currentWhere,
|
|
923
|
-
orderBy: currentOrderBy,
|
|
924
|
-
take: currentLimit,
|
|
925
|
-
skip: currentOffset
|
|
1044
|
+
where: currentWhere, // Use ref value
|
|
1045
|
+
orderBy: currentOrderBy, // Use ref value
|
|
1046
|
+
take: currentLimit, // Use ref value
|
|
1047
|
+
skip: currentOffset // Use ref value (passed as skip to findMany)
|
|
926
1048
|
});
|
|
927
1049
|
}, 0);
|
|
1050
|
+
|
|
1051
|
+
// Return the filtered data without resizing for now
|
|
1052
|
+
// The findMany call above will update the data later
|
|
1053
|
+
return filteredData;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Re-apply ordering to maintain consistency
|
|
1057
|
+
let sortedData = [...filteredData];
|
|
1058
|
+
|
|
1059
|
+
// Apply ordering if specified
|
|
1060
|
+
if (currentOrderBy) { // Use ref value
|
|
1061
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
1062
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
1063
|
+
? currentOrderBy
|
|
1064
|
+
: [currentOrderBy];
|
|
1065
|
+
|
|
1066
|
+
// Apply each sort in sequence
|
|
1067
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1068
|
+
// Check each orderBy clause in sequence
|
|
1069
|
+
for (const orderByClause of orderByArray) {
|
|
1070
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
1071
|
+
const aValue = a[field as keyof typeof a];
|
|
1072
|
+
const bValue = b[field as keyof typeof b];
|
|
1073
|
+
|
|
1074
|
+
// Skip if values are equal and move to next criterion
|
|
1075
|
+
if (aValue === bValue) continue;
|
|
1076
|
+
|
|
1077
|
+
// Use the compareValues function for proper type handling
|
|
1078
|
+
return compareValues(aValue, bValue, direction as 'asc' | 'desc');
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return 0; // Equal if all criteria match
|
|
1082
|
+
});
|
|
1083
|
+
} else if (hasCreatedAt) {
|
|
1084
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
1085
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1086
|
+
const aValue = a[createdAtField as keyof typeof a];
|
|
1087
|
+
const bValue = b[createdAtField as keyof typeof b];
|
|
1088
|
+
return compareValues(aValue, bValue, 'desc');
|
|
1089
|
+
});
|
|
928
1090
|
}
|
|
929
1091
|
|
|
930
|
-
return
|
|
1092
|
+
return sortedData;
|
|
931
1093
|
});
|
|
932
1094
|
}
|
|
933
1095
|
}
|
|
@@ -942,7 +1104,7 @@ export function createSuparismaHook<
|
|
|
942
1104
|
return () => {
|
|
943
1105
|
console.log(\`Unsubscribing from \${channelId}\`);
|
|
944
1106
|
if (channelRef.current) {
|
|
945
|
-
channelRef.current
|
|
1107
|
+
supabase.removeChannel(channelRef.current); // Correct way to remove channel
|
|
946
1108
|
channelRef.current = null;
|
|
947
1109
|
}
|
|
948
1110
|
|
|
@@ -951,7 +1113,7 @@ export function createSuparismaHook<
|
|
|
951
1113
|
searchTimeoutRef.current = null;
|
|
952
1114
|
}
|
|
953
1115
|
};
|
|
954
|
-
}, [realtime, channelName,
|
|
1116
|
+
}, [realtime, channelName, tableName, initialLoadRef]); // Removed where, orderBy, limit, offset from deps
|
|
955
1117
|
|
|
956
1118
|
// Create a memoized options object to prevent unnecessary re-renders
|
|
957
1119
|
const optionsRef = useRef({ where, orderBy, limit, offset });
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suparisma",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Moe03/suparisma"
|
|
9
|
+
},
|
|
6
10
|
"scripts": {
|
|
7
11
|
"build": "tsc",
|
|
8
12
|
"start": "node dist/index.js",
|