suparisma 1.0.7 → 1.0.9
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 +212 -1
- package/dist/generators/coreGenerator.js +329 -146
- package/dist/generators/hookGenerator.js +0 -1
- package/dist/generators/indexGenerator.js +0 -1
- package/dist/generators/supabaseClientGenerator.js +0 -1
- package/dist/generators/typeGenerator.js +65 -34
- package/dist/index.js +2 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
|
|
|
23
23
|
- [Filtering Data](#filtering-data)
|
|
24
24
|
- [⚠️ IMPORTANT: Using Dynamic Filters with React](#️-important-using-dynamic-filters-with-react)
|
|
25
25
|
- [Array Filtering](#array-filtering)
|
|
26
|
+
- [OR/AND Conditions](#orand-conditions)
|
|
26
27
|
- [Sorting Data](#sorting-data)
|
|
27
28
|
- [Pagination](#pagination)
|
|
28
29
|
- [Search Functionality](#search-functionality)
|
|
@@ -61,6 +62,7 @@ Suparisma bridges this gap by:
|
|
|
61
62
|
- 🔄 **Real-time updates by default** for all tables (with opt-out capability)
|
|
62
63
|
- 🔒 **Type-safe interfaces** for all database operations
|
|
63
64
|
- 🔍 **Full-text search** with configurable annotations *(currently under maintenance)*
|
|
65
|
+
- ⚡ **Advanced filtering** with OR/AND conditions for complex queries
|
|
64
66
|
- 🔢 **Pagination and sorting** built into every hook
|
|
65
67
|
- 🧩 **Prisma-like API** that feels familiar if you already use Prisma
|
|
66
68
|
- 📱 **Works with any React framework** including Next.js, Remix, etc.
|
|
@@ -580,6 +582,215 @@ function ProductFilter() {
|
|
|
580
582
|
}
|
|
581
583
|
```
|
|
582
584
|
|
|
585
|
+
### OR/AND Conditions
|
|
586
|
+
|
|
587
|
+
Suparisma supports powerful logical operations to combine multiple filter conditions:
|
|
588
|
+
|
|
589
|
+
#### OR Conditions - Match ANY condition
|
|
590
|
+
|
|
591
|
+
Use `OR` to find records that match **any** of the provided conditions:
|
|
592
|
+
|
|
593
|
+
```tsx
|
|
594
|
+
// Find users with name "John" OR email containing "@admin"
|
|
595
|
+
const { data: users } = useSuparisma.user({
|
|
596
|
+
where: {
|
|
597
|
+
OR: [
|
|
598
|
+
{ name: "John" },
|
|
599
|
+
{ email: { contains: "@admin" } }
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Complex OR with multiple field types
|
|
605
|
+
const { data: posts } = useSuparisma.post({
|
|
606
|
+
where: {
|
|
607
|
+
OR: [
|
|
608
|
+
{ title: { contains: "react" } },
|
|
609
|
+
{ tags: { has: ["typescript"] } },
|
|
610
|
+
{ author: { name: "John Doe" } },
|
|
611
|
+
{ publishedAt: { gte: new Date('2024-01-01') } }
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// OR with array operators
|
|
617
|
+
const { data: products } = useSuparisma.product({
|
|
618
|
+
where: {
|
|
619
|
+
OR: [
|
|
620
|
+
{ categories: { has: ["electronics"] } },
|
|
621
|
+
{ categories: { has: ["gaming"] } },
|
|
622
|
+
{ price: { lt: 100 } }
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
#### AND Conditions - Match ALL conditions
|
|
629
|
+
|
|
630
|
+
Use `AND` for explicit conjunction (though regular object properties are ANDed by default):
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
// Explicit AND conditions
|
|
634
|
+
const { data: premiumUsers } = useSuparisma.user({
|
|
635
|
+
where: {
|
|
636
|
+
AND: [
|
|
637
|
+
{ active: true },
|
|
638
|
+
{ subscriptionTier: "premium" },
|
|
639
|
+
{ lastLoginAt: { gte: new Date('2024-01-01') } }
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Mix AND with other conditions
|
|
645
|
+
const { data: qualifiedPosts } = useSuparisma.post({
|
|
646
|
+
where: {
|
|
647
|
+
published: true, // Regular condition (implicit AND)
|
|
648
|
+
AND: [
|
|
649
|
+
{ views: { gte: 1000 } },
|
|
650
|
+
{ likes: { gte: 100 } }
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
#### Combining OR and AND
|
|
657
|
+
|
|
658
|
+
Create complex nested logic by combining OR and AND:
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
// Advanced search: (Premium users OR moderators) AND active
|
|
662
|
+
const { data: privilegedUsers } = useSuparisma.user({
|
|
663
|
+
where: {
|
|
664
|
+
active: true, // Must be active
|
|
665
|
+
OR: [
|
|
666
|
+
{ role: "premium" },
|
|
667
|
+
{ role: "moderator" }
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Content filtering: (Recent posts OR popular posts) AND published
|
|
673
|
+
const { data: featuredPosts } = useSuparisma.post({
|
|
674
|
+
where: {
|
|
675
|
+
published: true,
|
|
676
|
+
OR: [
|
|
677
|
+
{ createdAt: { gte: new Date('2024-01-01') } }, // Recent
|
|
678
|
+
{ views: { gte: 10000 } } // Popular
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Complex search across multiple fields and conditions
|
|
684
|
+
const { data: searchResults } = useSuparisma.product({
|
|
685
|
+
where: {
|
|
686
|
+
active: true,
|
|
687
|
+
AND: [
|
|
688
|
+
{
|
|
689
|
+
OR: [
|
|
690
|
+
{ name: { contains: searchTerm } },
|
|
691
|
+
{ description: { contains: searchTerm } },
|
|
692
|
+
{ tags: { has: [searchTerm] } }
|
|
693
|
+
]
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
OR: [
|
|
697
|
+
{ price: { between: [minPrice, maxPrice] } },
|
|
698
|
+
{ onSale: true }
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
#### Real-World Examples
|
|
707
|
+
|
|
708
|
+
**Multi-field search for e-commerce:**
|
|
709
|
+
```tsx
|
|
710
|
+
const { data: products } = useSuparisma.product({
|
|
711
|
+
where: {
|
|
712
|
+
OR: [
|
|
713
|
+
{ name: { contains: "laptop" } },
|
|
714
|
+
{ description: { contains: "laptop" } },
|
|
715
|
+
{ categories: { has: ["computers", "electronics"] } },
|
|
716
|
+
{ brand: { in: ["Apple", "Dell", "HP"] } }
|
|
717
|
+
]
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**User permission filtering:**
|
|
723
|
+
```tsx
|
|
724
|
+
const { data: accessiblePosts } = useSuparisma.post({
|
|
725
|
+
where: {
|
|
726
|
+
OR: [
|
|
727
|
+
{ public: true },
|
|
728
|
+
{ authorId: currentUserId },
|
|
729
|
+
{
|
|
730
|
+
AND: [
|
|
731
|
+
{ published: true },
|
|
732
|
+
{ collaborators: { has: [currentUserId] } }
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Dynamic search with React state:**
|
|
741
|
+
```tsx
|
|
742
|
+
import { useMemo } from 'react';
|
|
743
|
+
|
|
744
|
+
function SearchComponent() {
|
|
745
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
746
|
+
const [category, setCategory] = useState("");
|
|
747
|
+
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
|
|
748
|
+
|
|
749
|
+
// ✅ Use useMemo for stable object reference
|
|
750
|
+
const searchFilter = useMemo(() => {
|
|
751
|
+
const conditions = [];
|
|
752
|
+
|
|
753
|
+
// Add search term condition
|
|
754
|
+
if (searchTerm) {
|
|
755
|
+
conditions.push({
|
|
756
|
+
OR: [
|
|
757
|
+
{ name: { contains: searchTerm } },
|
|
758
|
+
{ description: { contains: searchTerm } },
|
|
759
|
+
{ tags: { has: [searchTerm] } }
|
|
760
|
+
]
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Add category condition
|
|
765
|
+
if (category) {
|
|
766
|
+
conditions.push({
|
|
767
|
+
categories: { has: [category] }
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Add price range condition
|
|
772
|
+
conditions.push({
|
|
773
|
+
price: { gte: priceRange.min, lte: priceRange.max }
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
return conditions.length > 0 ? { AND: conditions } : undefined;
|
|
777
|
+
}, [searchTerm, category, priceRange]);
|
|
778
|
+
|
|
779
|
+
const { data: products } = useSuparisma.product({
|
|
780
|
+
where: searchFilter
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// ... rest of component
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### Important Notes
|
|
788
|
+
|
|
789
|
+
1. **Performance**: OR conditions may require client-side filtering for realtime updates
|
|
790
|
+
2. **Realtime**: Complex OR/AND combinations automatically enable client-side filtering mode
|
|
791
|
+
3. **Memory**: Use `useMemo` for dynamic OR/AND filters to maintain stable object references
|
|
792
|
+
4. **Nesting**: You can nest OR within AND and vice versa for complex logic
|
|
793
|
+
|
|
583
794
|
### Sorting Data
|
|
584
795
|
|
|
585
796
|
Sort data using Prisma-like ordering:
|
|
@@ -955,7 +1166,7 @@ async function handleSubmit(event) {
|
|
|
955
1166
|
name: formData.name,
|
|
956
1167
|
someNumber: parseInt(formData.number)
|
|
957
1168
|
});
|
|
958
|
-
|
|
1169
|
+
// Handle successful creation
|
|
959
1170
|
} catch (err) {
|
|
960
1171
|
console.error('Failed to create thing:', err);
|
|
961
1172
|
}
|
|
@@ -94,6 +94,21 @@ export type FilterOperators<T> = {
|
|
|
94
94
|
// Type for a single field in an advanced where filter
|
|
95
95
|
export type AdvancedWhereInput<T> = {
|
|
96
96
|
[K in keyof T]?: T[K] | FilterOperators<T[K]>;
|
|
97
|
+
} & {
|
|
98
|
+
/**
|
|
99
|
+
* OR condition - match records that satisfy ANY of the provided conditions
|
|
100
|
+
* @example
|
|
101
|
+
* // Find users where name is "John" OR email contains "@admin"
|
|
102
|
+
* { OR: [{ name: "John" }, { email: { contains: "@admin" } }] }
|
|
103
|
+
*/
|
|
104
|
+
OR?: AdvancedWhereInput<T>[];
|
|
105
|
+
/**
|
|
106
|
+
* AND condition - match records that satisfy ALL of the provided conditions (default behavior)
|
|
107
|
+
* @example
|
|
108
|
+
* // Find users where name is "John" AND age > 18
|
|
109
|
+
* { AND: [{ name: "John" }, { age: { gt: 18 } }] }
|
|
110
|
+
*/
|
|
111
|
+
AND?: AdvancedWhereInput<T>[];
|
|
97
112
|
};
|
|
98
113
|
|
|
99
114
|
/**
|
|
@@ -177,6 +192,19 @@ export type SearchState = {
|
|
|
177
192
|
clearQueries: () => void;
|
|
178
193
|
};
|
|
179
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Escape values for Supabase filter strings
|
|
197
|
+
*/
|
|
198
|
+
function escapeFilterValue(value: any): string {
|
|
199
|
+
if (typeof value === 'string') {
|
|
200
|
+
// If the string contains spaces or special characters, wrap it in quotes
|
|
201
|
+
if (value.includes(' ') || value.includes(',') || value.includes('(') || value.includes(')')) {
|
|
202
|
+
return \`"\${value.replace(/"/g, '\\\\"')}"\`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return String(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
/**
|
|
181
209
|
* Compare two values for sorting with proper type handling
|
|
182
210
|
*/
|
|
@@ -215,9 +243,73 @@ export function buildFilterString<T>(where?: T): string | undefined {
|
|
|
215
243
|
if (!where) return undefined;
|
|
216
244
|
|
|
217
245
|
const filters: string[] = [];
|
|
246
|
+
const orConditions: string[] = [];
|
|
218
247
|
|
|
219
248
|
for (const [key, value] of Object.entries(where)) {
|
|
220
249
|
if (value !== undefined) {
|
|
250
|
+
// Handle OR conditions
|
|
251
|
+
if (key === 'OR' && Array.isArray(value)) {
|
|
252
|
+
const orParts: string[] = [];
|
|
253
|
+
for (const orCondition of value) {
|
|
254
|
+
// Build individual OR condition parts using proper dot notation
|
|
255
|
+
for (const [orKey, orValue] of Object.entries(orCondition)) {
|
|
256
|
+
if (orValue !== undefined) {
|
|
257
|
+
if (typeof orValue === 'object' && orValue !== null) {
|
|
258
|
+
// Handle advanced operators in OR conditions
|
|
259
|
+
const advancedOps = orValue as unknown as FilterOperators<any>;
|
|
260
|
+
|
|
261
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
262
|
+
orParts.push(\`\${orKey}.eq.\${advancedOps.equals}\`);
|
|
263
|
+
} else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
264
|
+
orParts.push(\`\${orKey}.ilike.*\${advancedOps.contains}*\`);
|
|
265
|
+
} else if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
266
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
267
|
+
const arrayValue = \`{\${advancedOps.has.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
268
|
+
orParts.push(\`\${orKey}.ov.\${arrayValue}\`);
|
|
269
|
+
} else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
270
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
271
|
+
const arrayValue = \`{\${advancedOps.hasEvery.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
272
|
+
orParts.push(\`\${orKey}.cs.\${arrayValue}\`);
|
|
273
|
+
} else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
274
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
275
|
+
const arrayValue = \`{\${advancedOps.hasSome.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
276
|
+
orParts.push(\`\${orKey}.ov.\${arrayValue}\`);
|
|
277
|
+
} else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
278
|
+
orParts.push(\`\${orKey}.gt.\${advancedOps.gt}\`);
|
|
279
|
+
} else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
280
|
+
orParts.push(\`\${orKey}.gte.\${advancedOps.gte}\`);
|
|
281
|
+
} else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
282
|
+
orParts.push(\`\${orKey}.lt.\${advancedOps.lt}\`);
|
|
283
|
+
} else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
284
|
+
orParts.push(\`\${orKey}.lte.\${advancedOps.lte}\`);
|
|
285
|
+
} else if ('in' in advancedOps && advancedOps.in?.length) {
|
|
286
|
+
orParts.push(\`\${orKey}.in.(\${advancedOps.in.join(',')})\`);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Simple equality in OR condition
|
|
290
|
+
orParts.push(\`\${orKey}.eq.\${escapeFilterValue(orValue)}\`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (orParts.length > 0) {
|
|
297
|
+
orConditions.push(\`or(\${orParts.join(',')})\`);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Handle AND conditions (explicit)
|
|
303
|
+
if (key === 'AND' && Array.isArray(value)) {
|
|
304
|
+
for (const andCondition of value) {
|
|
305
|
+
const andFilter = buildFilterString(andCondition);
|
|
306
|
+
if (andFilter) {
|
|
307
|
+
filters.push(andFilter);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
221
313
|
if (typeof value === 'object' && value !== null) {
|
|
222
314
|
// Handle advanced operators
|
|
223
315
|
const advancedOps = value as unknown as FilterOperators<any>;
|
|
@@ -265,19 +357,22 @@ export function buildFilterString<T>(where?: T): string | undefined {
|
|
|
265
357
|
// Array-specific operators
|
|
266
358
|
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
267
359
|
// Array contains ANY of the specified items (overlaps)
|
|
268
|
-
|
|
360
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
361
|
+
const arrayValue = \`{\${advancedOps.has.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
269
362
|
filters.push(\`\${key}=ov.\${arrayValue}\`);
|
|
270
363
|
}
|
|
271
364
|
|
|
272
365
|
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
273
366
|
// Array contains ALL of the specified items (contains)
|
|
274
|
-
|
|
367
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
368
|
+
const arrayValue = \`{\${advancedOps.hasEvery.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
275
369
|
filters.push(\`\${key}=cs.\${arrayValue}\`);
|
|
276
370
|
}
|
|
277
371
|
|
|
278
372
|
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
279
373
|
// Array contains ANY of the specified items (overlaps)
|
|
280
|
-
|
|
374
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
375
|
+
const arrayValue = \`{\${advancedOps.hasSome.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
281
376
|
filters.push(\`\${key}=ov.\${arrayValue}\`);
|
|
282
377
|
}
|
|
283
378
|
|
|
@@ -297,7 +392,18 @@ export function buildFilterString<T>(where?: T): string | undefined {
|
|
|
297
392
|
}
|
|
298
393
|
}
|
|
299
394
|
|
|
300
|
-
|
|
395
|
+
// Combine filters with OR conditions
|
|
396
|
+
const allConditions: string[] = [];
|
|
397
|
+
|
|
398
|
+
if (filters.length > 0) {
|
|
399
|
+
allConditions.push(filters.join(','));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (orConditions.length > 0) {
|
|
403
|
+
allConditions.push(\`or(\${orConditions.join(',')})\`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return allConditions.length > 0 ? allConditions.join(',') : undefined;
|
|
301
407
|
}
|
|
302
408
|
|
|
303
409
|
/**
|
|
@@ -314,6 +420,69 @@ export function applyFilter<T>(
|
|
|
314
420
|
// Apply each filter condition
|
|
315
421
|
for (const [key, value] of Object.entries(where)) {
|
|
316
422
|
if (value !== undefined) {
|
|
423
|
+
// Handle OR conditions
|
|
424
|
+
if (key === 'OR' && Array.isArray(value)) {
|
|
425
|
+
// For OR conditions, build the proper dot notation filter string
|
|
426
|
+
const orParts: string[] = [];
|
|
427
|
+
|
|
428
|
+
for (const orCondition of value) {
|
|
429
|
+
for (const [orKey, orValue] of Object.entries(orCondition)) {
|
|
430
|
+
if (orValue !== undefined) {
|
|
431
|
+
if (typeof orValue === 'object' && orValue !== null) {
|
|
432
|
+
// Handle advanced operators in OR conditions
|
|
433
|
+
const advancedOps = orValue as unknown as FilterOperators<any>;
|
|
434
|
+
|
|
435
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
436
|
+
orParts.push(\`\${orKey}.eq.\${advancedOps.equals}\`);
|
|
437
|
+
} else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
438
|
+
orParts.push(\`\${orKey}.ilike.*\${advancedOps.contains}*\`);
|
|
439
|
+
} else if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
440
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
441
|
+
const arrayValue = \`{\${advancedOps.has.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
442
|
+
orParts.push(\`\${orKey}.ov.\${arrayValue}\`);
|
|
443
|
+
} else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
444
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
445
|
+
const arrayValue = \`{\${advancedOps.hasEvery.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
446
|
+
orParts.push(\`\${orKey}.cs.\${arrayValue}\`);
|
|
447
|
+
} else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
448
|
+
// Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
|
|
449
|
+
const arrayValue = \`{\${advancedOps.hasSome.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
|
|
450
|
+
orParts.push(\`\${orKey}.ov.\${arrayValue}\`);
|
|
451
|
+
} else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
452
|
+
orParts.push(\`\${orKey}.gt.\${advancedOps.gt}\`);
|
|
453
|
+
} else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
454
|
+
orParts.push(\`\${orKey}.gte.\${advancedOps.gte}\`);
|
|
455
|
+
} else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
456
|
+
orParts.push(\`\${orKey}.lt.\${advancedOps.lt}\`);
|
|
457
|
+
} else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
458
|
+
orParts.push(\`\${orKey}.lte.\${advancedOps.lte}\`);
|
|
459
|
+
} else if ('in' in advancedOps && advancedOps.in?.length) {
|
|
460
|
+
orParts.push(\`\${orKey}.in.(\${advancedOps.in.join(',')})\`);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
// Simple equality in OR condition
|
|
464
|
+
orParts.push(\`\${orKey}.eq.\${escapeFilterValue(orValue)}\`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (orParts.length > 0) {
|
|
471
|
+
// @ts-ignore: Supabase typing issue
|
|
472
|
+
filteredQuery = filteredQuery.or(orParts.join(','));
|
|
473
|
+
}
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Handle AND conditions (explicit)
|
|
478
|
+
if (key === 'AND' && Array.isArray(value)) {
|
|
479
|
+
// For explicit AND conditions, apply each condition normally
|
|
480
|
+
for (const andCondition of value) {
|
|
481
|
+
filteredQuery = applyFilter(filteredQuery, andCondition);
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
317
486
|
if (typeof value === 'object' && value !== null) {
|
|
318
487
|
// Handle advanced operators
|
|
319
488
|
const advancedOps = value as unknown as FilterOperators<any>;
|
|
@@ -445,6 +614,111 @@ export function applyOrderBy<T>(
|
|
|
445
614
|
return orderedQuery;
|
|
446
615
|
}
|
|
447
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Client-side filter validation for realtime events
|
|
619
|
+
*/
|
|
620
|
+
function clientSideFilterCheck<T>(record: any, where: T): boolean {
|
|
621
|
+
if (!where) return true;
|
|
622
|
+
|
|
623
|
+
for (const [key, value] of Object.entries(where)) {
|
|
624
|
+
if (value !== undefined) {
|
|
625
|
+
// Handle OR conditions
|
|
626
|
+
if (key === 'OR' && Array.isArray(value)) {
|
|
627
|
+
// For OR, at least one condition must match
|
|
628
|
+
const orMatches = value.some(orCondition => clientSideFilterCheck(record, orCondition));
|
|
629
|
+
if (!orMatches) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Handle AND conditions (explicit)
|
|
636
|
+
if (key === 'AND' && Array.isArray(value)) {
|
|
637
|
+
// For AND, all conditions must match
|
|
638
|
+
const andMatches = value.every(andCondition => clientSideFilterCheck(record, andCondition));
|
|
639
|
+
if (!andMatches) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (typeof value === 'object' && value !== null) {
|
|
646
|
+
// Handle complex array filters client-side
|
|
647
|
+
const advancedOps = value as any;
|
|
648
|
+
const recordValue = record[key as keyof typeof record] as any;
|
|
649
|
+
|
|
650
|
+
// Array-specific operators validation
|
|
651
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
652
|
+
// Array contains ANY of the specified items
|
|
653
|
+
if (!Array.isArray(recordValue) || !advancedOps.has.some((item: any) => recordValue.includes(item))) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
} else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
657
|
+
// Array contains ALL of the specified items
|
|
658
|
+
if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item: any) => recordValue.includes(item))) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
} else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
662
|
+
// Array contains ANY of the specified items
|
|
663
|
+
if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item: any) => recordValue.includes(item))) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
} else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
667
|
+
// Array is empty or not empty
|
|
668
|
+
const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
|
|
669
|
+
if (isEmpty !== advancedOps.isEmpty) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
} else if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
673
|
+
if (recordValue !== advancedOps.equals) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
} else if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
677
|
+
if (recordValue === advancedOps.not) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
} else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
681
|
+
if (!(recordValue > advancedOps.gt)) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
} else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
685
|
+
if (!(recordValue >= advancedOps.gte)) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
} else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
689
|
+
if (!(recordValue < advancedOps.lt)) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
} else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
693
|
+
if (!(recordValue <= advancedOps.lte)) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
} else if ('in' in advancedOps && advancedOps.in !== undefined) {
|
|
697
|
+
if (!advancedOps.in.includes(recordValue)) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
} else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
701
|
+
if (!String(recordValue).toLowerCase().includes(String(advancedOps.contains).toLowerCase())) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
} else if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
705
|
+
if (!String(recordValue).toLowerCase().startsWith(String(advancedOps.startsWith).toLowerCase())) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
} else if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
709
|
+
if (!String(recordValue).toLowerCase().endsWith(String(advancedOps.endsWith).toLowerCase())) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} else if (record[key as keyof typeof record] !== value) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
|
|
448
722
|
/**
|
|
449
723
|
* Core hook factory function that creates a type-safe realtime hook for a specific model.
|
|
450
724
|
* This is the foundation for all Suparisma hooks.
|
|
@@ -488,12 +762,23 @@ export function createSuparismaHook<
|
|
|
488
762
|
* const users = useSuparismaUser();
|
|
489
763
|
* const { data, loading, error } = users;
|
|
490
764
|
*
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
765
|
+
* @example
|
|
766
|
+
* // With filtering
|
|
767
|
+
* const users = useSuparismaUser({
|
|
768
|
+
* where: { role: 'admin' },
|
|
769
|
+
* orderBy: { created_at: 'desc' }
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* // With OR conditions for search
|
|
774
|
+
* const users = useSuparismaUser({
|
|
775
|
+
* where: {
|
|
776
|
+
* OR: [
|
|
777
|
+
* { name: { contains: "john" } },
|
|
778
|
+
* { email: { contains: "john" } }
|
|
779
|
+
* ]
|
|
780
|
+
* }
|
|
781
|
+
* });
|
|
497
782
|
*/
|
|
498
783
|
return function useSuparismaHook(options: SuparismaOptions<TWhereInput, TOrderByInput> = {}) {
|
|
499
784
|
const {
|
|
@@ -704,19 +989,7 @@ export function createSuparismaHook<
|
|
|
704
989
|
|
|
705
990
|
// Apply any where conditions client-side
|
|
706
991
|
if (where) {
|
|
707
|
-
results = results.filter((item) =>
|
|
708
|
-
for (const [key, value] of Object.entries(where)) {
|
|
709
|
-
if (typeof value === 'object' && value !== null) {
|
|
710
|
-
// Skip complex filters for now
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (item[key as keyof typeof item] !== value) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return true;
|
|
719
|
-
});
|
|
992
|
+
results = results.filter((item) => clientSideFilterCheck(item, where));
|
|
720
993
|
}
|
|
721
994
|
|
|
722
995
|
// Set count directly for search results
|
|
@@ -912,19 +1185,33 @@ export function createSuparismaHook<
|
|
|
912
1185
|
|
|
913
1186
|
const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
|
|
914
1187
|
|
|
915
|
-
// Check if we have complex array filters that should be handled client-side only
|
|
1188
|
+
// Check if we have complex array filters or OR/AND conditions that should be handled client-side only
|
|
916
1189
|
let hasComplexArrayFilters = false;
|
|
917
1190
|
if (where) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1191
|
+
const checkForComplexFilters = (whereClause: any): boolean => {
|
|
1192
|
+
for (const [key, value] of Object.entries(whereClause)) {
|
|
1193
|
+
// OR/AND conditions require client-side filtering
|
|
1194
|
+
if (key === 'OR' || key === 'AND') {
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (typeof value === 'object' && value !== null) {
|
|
1199
|
+
// Check if it's an array of conditions (nested OR/AND)
|
|
1200
|
+
if (Array.isArray(value)) {
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const advancedOps = value as any;
|
|
1205
|
+
// Check for complex array operators
|
|
1206
|
+
if ('has' in advancedOps || 'hasEvery' in advancedOps || 'hasSome' in advancedOps || 'isEmpty' in advancedOps) {
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
925
1209
|
}
|
|
926
1210
|
}
|
|
927
|
-
|
|
1211
|
+
return false;
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
hasComplexArrayFilters = checkForComplexFilters(where);
|
|
928
1215
|
}
|
|
929
1216
|
|
|
930
1217
|
// For complex array filters, use no database filter and rely on client-side filtering
|
|
@@ -937,18 +1224,15 @@ export function createSuparismaHook<
|
|
|
937
1224
|
|
|
938
1225
|
if (hasComplexArrayFilters) {
|
|
939
1226
|
// Don't include filter at all for complex array operations
|
|
940
|
-
console.log(\`Setting up subscription for \${tableName} with NO FILTER (complex array filters detected) - will receive ALL events\`);
|
|
941
1227
|
} else if (where) {
|
|
942
1228
|
// Include filter for simple operations
|
|
943
1229
|
const filter = buildFilterString(where);
|
|
944
1230
|
if (filter) {
|
|
945
1231
|
subscriptionConfig.filter = filter;
|
|
946
1232
|
}
|
|
947
|
-
console.log(\`Setting up subscription for \${tableName} with database filter: \${filter}\`);
|
|
948
1233
|
} else if (realtimeFilter) {
|
|
949
1234
|
// Use custom realtime filter if provided
|
|
950
1235
|
subscriptionConfig.filter = realtimeFilter;
|
|
951
|
-
console.log(\`Setting up subscription for \${tableName} with custom filter: \${realtimeFilter}\`);
|
|
952
1236
|
}
|
|
953
1237
|
|
|
954
1238
|
const channel = supabase
|
|
@@ -957,7 +1241,6 @@ export function createSuparismaHook<
|
|
|
957
1241
|
'postgres_changes',
|
|
958
1242
|
subscriptionConfig,
|
|
959
1243
|
(payload) => {
|
|
960
|
-
console.log(\`🔥 REALTIME EVENT RECEIVED for \${tableName}:\`, payload.eventType, payload);
|
|
961
1244
|
|
|
962
1245
|
// Access current options via refs inside the event handler
|
|
963
1246
|
const currentWhere = whereRef.current;
|
|
@@ -967,7 +1250,6 @@ export function createSuparismaHook<
|
|
|
967
1250
|
|
|
968
1251
|
// Skip realtime updates when search is active
|
|
969
1252
|
if (isSearchingRef.current) {
|
|
970
|
-
console.log('⏭️ Skipping realtime update - search is active');
|
|
971
1253
|
return;
|
|
972
1254
|
}
|
|
973
1255
|
|
|
@@ -976,57 +1258,13 @@ export function createSuparismaHook<
|
|
|
976
1258
|
setData((prev) => {
|
|
977
1259
|
try {
|
|
978
1260
|
const newRecord = payload.new as TWithRelations;
|
|
979
|
-
console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
|
|
980
1261
|
|
|
981
1262
|
// ALWAYS check if this record matches our filter client-side
|
|
982
1263
|
// This is especially important for complex array filters
|
|
983
1264
|
if (currentWhere) { // Use ref value
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
// Check each filter condition client-side for complex filters
|
|
987
|
-
for (const [key, value] of Object.entries(currentWhere)) {
|
|
988
|
-
if (typeof value === 'object' && value !== null) {
|
|
989
|
-
// Handle complex array filters client-side
|
|
990
|
-
const advancedOps = value as any;
|
|
991
|
-
const recordValue = newRecord[key as keyof typeof newRecord] as any;
|
|
992
|
-
|
|
993
|
-
// Array-specific operators validation
|
|
994
|
-
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
995
|
-
// Array contains ANY of the specified items
|
|
996
|
-
if (!Array.isArray(recordValue) || !advancedOps.has.some((item: any) => recordValue.includes(item))) {
|
|
997
|
-
matchesFilter = false;
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
} else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
1001
|
-
// Array contains ALL of the specified items
|
|
1002
|
-
if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item: any) => recordValue.includes(item))) {
|
|
1003
|
-
matchesFilter = false;
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
} else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
1007
|
-
// Array contains ANY of the specified items
|
|
1008
|
-
if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item: any) => recordValue.includes(item))) {
|
|
1009
|
-
matchesFilter = false;
|
|
1010
|
-
break;
|
|
1011
|
-
}
|
|
1012
|
-
} else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
1013
|
-
// Array is empty or not empty
|
|
1014
|
-
const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
|
|
1015
|
-
if (isEmpty !== advancedOps.isEmpty) {
|
|
1016
|
-
matchesFilter = false;
|
|
1017
|
-
break;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
// Add other complex filter validations as needed
|
|
1021
|
-
} else if (newRecord[key as keyof typeof newRecord] !== value) {
|
|
1022
|
-
matchesFilter = false;
|
|
1023
|
-
console.log(\`Filter mismatch on \${key}\`, { expected: value, actual: newRecord[key as keyof typeof newRecord] });
|
|
1024
|
-
break;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1265
|
+
const matchesFilter = clientSideFilterCheck(newRecord, currentWhere);
|
|
1027
1266
|
|
|
1028
1267
|
if (!matchesFilter) {
|
|
1029
|
-
console.log('New record does not match filter criteria, skipping');
|
|
1030
1268
|
return prev;
|
|
1031
1269
|
}
|
|
1032
1270
|
}
|
|
@@ -1038,7 +1276,6 @@ export function createSuparismaHook<
|
|
|
1038
1276
|
);
|
|
1039
1277
|
|
|
1040
1278
|
if (exists) {
|
|
1041
|
-
console.log('Record already exists, skipping insertion');
|
|
1042
1279
|
return prev;
|
|
1043
1280
|
}
|
|
1044
1281
|
|
|
@@ -1109,50 +1346,10 @@ export function createSuparismaHook<
|
|
|
1109
1346
|
|
|
1110
1347
|
// Check if the updated record still matches our current filter
|
|
1111
1348
|
if (currentWhere) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
for (const [key, value] of Object.entries(currentWhere)) {
|
|
1115
|
-
if (typeof value === 'object' && value !== null) {
|
|
1116
|
-
// Handle complex array filters client-side
|
|
1117
|
-
const advancedOps = value as any;
|
|
1118
|
-
const recordValue = updatedRecord[key as keyof typeof updatedRecord] as any;
|
|
1119
|
-
|
|
1120
|
-
// Array-specific operators validation
|
|
1121
|
-
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
1122
|
-
// Array contains ANY of the specified items
|
|
1123
|
-
if (!Array.isArray(recordValue) || !advancedOps.has.some((item: any) => recordValue.includes(item))) {
|
|
1124
|
-
matchesFilter = false;
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
} else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
1128
|
-
// Array contains ALL of the specified items
|
|
1129
|
-
if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item: any) => recordValue.includes(item))) {
|
|
1130
|
-
matchesFilter = false;
|
|
1131
|
-
break;
|
|
1132
|
-
}
|
|
1133
|
-
} else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
1134
|
-
// Array contains ANY of the specified items
|
|
1135
|
-
if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item: any) => recordValue.includes(item))) {
|
|
1136
|
-
matchesFilter = false;
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
} else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
1140
|
-
// Array is empty or not empty
|
|
1141
|
-
const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
|
|
1142
|
-
if (isEmpty !== advancedOps.isEmpty) {
|
|
1143
|
-
matchesFilter = false;
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
} else if (updatedRecord[key as keyof typeof updatedRecord] !== value) {
|
|
1148
|
-
matchesFilter = false;
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1349
|
+
const matchesFilter = clientSideFilterCheck(updatedRecord, currentWhere);
|
|
1152
1350
|
|
|
1153
1351
|
// If the updated record doesn't match the filter, remove it from the list
|
|
1154
1352
|
if (!matchesFilter) {
|
|
1155
|
-
console.log('Updated record no longer matches filter, removing from list');
|
|
1156
1353
|
return prev.filter((item) =>
|
|
1157
1354
|
// @ts-ignore: Supabase typing issue
|
|
1158
1355
|
!('id' in item && 'id' in updatedRecord && item.id === updatedRecord.id)
|
|
@@ -1211,9 +1408,7 @@ export function createSuparismaHook<
|
|
|
1211
1408
|
});
|
|
1212
1409
|
} else if (payload.eventType === 'DELETE') {
|
|
1213
1410
|
// Process delete event
|
|
1214
|
-
console.log('🗑️ Processing DELETE event for', tableName);
|
|
1215
1411
|
setData((prev) => {
|
|
1216
|
-
console.log('🗑️ DELETE: Current data before deletion:', prev.length, 'items');
|
|
1217
1412
|
|
|
1218
1413
|
// Access current options via refs
|
|
1219
1414
|
const currentWhere = whereRef.current;
|
|
@@ -1223,7 +1418,6 @@ export function createSuparismaHook<
|
|
|
1223
1418
|
|
|
1224
1419
|
// Skip if search is active
|
|
1225
1420
|
if (isSearchingRef.current) {
|
|
1226
|
-
console.log('⏭️ DELETE: Skipping - search is active');
|
|
1227
1421
|
return prev;
|
|
1228
1422
|
}
|
|
1229
1423
|
|
|
@@ -1233,21 +1427,14 @@ export function createSuparismaHook<
|
|
|
1233
1427
|
// Filter out the deleted item
|
|
1234
1428
|
const filteredData = prev.filter((item) => {
|
|
1235
1429
|
// @ts-ignore: Supabase typing issue
|
|
1236
|
-
|
|
1237
|
-
if (!shouldKeep) {
|
|
1238
|
-
console.log('🗑️ DELETE: Removing item with ID:', item.id);
|
|
1239
|
-
}
|
|
1240
|
-
return shouldKeep;
|
|
1430
|
+
return !('id' in item && 'id' in payload.old && item.id === payload.old.id);
|
|
1241
1431
|
});
|
|
1242
1432
|
|
|
1243
|
-
console.log('🗑️ DELETE: Data after deletion:', filteredData.length, 'items (was', currentSize, ')');
|
|
1244
|
-
|
|
1245
1433
|
// Fetch the updated count after the data changes
|
|
1246
1434
|
setTimeout(() => fetchTotalCount(), 0);
|
|
1247
1435
|
|
|
1248
1436
|
// If we need to maintain the size with a limit
|
|
1249
1437
|
if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) { // Use ref value
|
|
1250
|
-
console.log(\`🗑️ DELETE: Record deleted with limit \${currentLimit}, will fetch additional record to maintain size\`);
|
|
1251
1438
|
|
|
1252
1439
|
// Use setTimeout to ensure this state update completes first
|
|
1253
1440
|
setTimeout(() => {
|
|
@@ -1305,19 +1492,16 @@ export function createSuparismaHook<
|
|
|
1305
1492
|
}
|
|
1306
1493
|
}
|
|
1307
1494
|
)
|
|
1308
|
-
.subscribe(
|
|
1309
|
-
console.log(\`Subscription status for \${tableName}\`, status);
|
|
1310
|
-
});
|
|
1495
|
+
.subscribe();
|
|
1311
1496
|
|
|
1312
1497
|
// Store the channel ref
|
|
1313
1498
|
channelRef.current = channel;
|
|
1314
1499
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
}
|
|
1500
|
+
return () => {
|
|
1501
|
+
if (channelRef.current) {
|
|
1502
|
+
supabase.removeChannel(channelRef.current); // Correct way to remove channel
|
|
1503
|
+
channelRef.current = null;
|
|
1504
|
+
}
|
|
1321
1505
|
|
|
1322
1506
|
if (searchTimeoutRef.current) {
|
|
1323
1507
|
clearTimeout(searchTimeoutRef.current);
|
|
@@ -1362,7 +1546,6 @@ export function createSuparismaHook<
|
|
|
1362
1546
|
if (initialLoadRef.current) {
|
|
1363
1547
|
// Only reload if options have changed significantly
|
|
1364
1548
|
if (optionsChanged()) {
|
|
1365
|
-
console.log(\`Options changed for \${tableName}, reloading data\`);
|
|
1366
1549
|
findMany({
|
|
1367
1550
|
where,
|
|
1368
1551
|
orderBy,
|
|
@@ -1874,6 +2057,7 @@ export function createSuparismaHook<
|
|
|
1874
2057
|
: api;
|
|
1875
2058
|
};
|
|
1876
2059
|
}
|
|
2060
|
+
|
|
1877
2061
|
`; // Ensure template literal is closed
|
|
1878
2062
|
// Output to the UTILS_DIR
|
|
1879
2063
|
const outputPath = path_1.default.join(config_1.UTILS_DIR, 'core.ts');
|
|
@@ -1881,5 +2065,4 @@ export function createSuparismaHook<
|
|
|
1881
2065
|
fs_1.default.mkdirSync(config_1.UTILS_DIR, { recursive: true });
|
|
1882
2066
|
}
|
|
1883
2067
|
fs_1.default.writeFileSync(outputPath, coreContent);
|
|
1884
|
-
console.log(`Generated core utility file at ${outputPath}`);
|
|
1885
2068
|
}
|
|
@@ -113,5 +113,4 @@ export const ${config_1.HOOK_NAME_PREFIX}${modelName} = createSuparismaHook<
|
|
|
113
113
|
fs_1.default.mkdirSync(config_1.HOOKS_DIR, { recursive: true });
|
|
114
114
|
}
|
|
115
115
|
fs_1.default.writeFileSync(outputPath, hookContent);
|
|
116
|
-
console.log(`Generated hook for ${modelName} at ${outputPath}`);
|
|
117
116
|
}
|
|
@@ -118,40 +118,44 @@ function generateModelTypesFile(model) {
|
|
|
118
118
|
// Generate imports section for zod custom types
|
|
119
119
|
let customImports = '';
|
|
120
120
|
if (model.zodImports && model.zodImports.length > 0) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
try {
|
|
122
|
+
// Get the zod schemas file path from environment variable
|
|
123
|
+
const zodSchemasPath = process.env.ZOD_SCHEMAS_FILE_PATH || '../commonTypes';
|
|
124
|
+
// Add custom imports with environment variable path, but make them conditional
|
|
125
|
+
const importStatements = model.zodImports
|
|
126
|
+
.map(zodImport => {
|
|
127
|
+
// Extract the types from the original import statement
|
|
128
|
+
const typeMatch = zodImport.importStatement.match(/import\s+{\s*([^}]+)\s*}\s+from/);
|
|
129
|
+
if (typeMatch) {
|
|
130
|
+
const types = typeMatch[1].trim();
|
|
131
|
+
return `// Optional: install zod and create the commonTypes file if you want type safety for JSON fields\n// import { ${types} } from '${zodSchemasPath}'`;
|
|
132
|
+
}
|
|
133
|
+
return `// ${zodImport.importStatement}`;
|
|
134
|
+
})
|
|
135
|
+
.join('\n') + '\n\n';
|
|
136
|
+
customImports += importStatements;
|
|
137
|
+
// Add type definitions for imported zod schemas as simple types
|
|
138
|
+
const customTypeDefinitions = model.zodImports
|
|
139
|
+
.flatMap(zodImport => zodImport.types)
|
|
140
|
+
.filter((type, index, array) => array.indexOf(type) === index) // Remove duplicates
|
|
141
|
+
.map(type => {
|
|
142
|
+
// If it ends with 'Schema', create a corresponding type as any for now
|
|
143
|
+
if (type.endsWith('Schema')) {
|
|
144
|
+
const typeName = type.replace('Schema', '');
|
|
145
|
+
return `// Fallback type - replace with actual type if you implement zod schemas\nexport type ${typeName} = any;`;
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
})
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join('\n');
|
|
151
|
+
if (customTypeDefinitions) {
|
|
152
|
+
customImports += customTypeDefinitions + '\n\n';
|
|
133
153
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
// This is a simplified approach - you might want to make this more sophisticated
|
|
140
|
-
const customTypeDefinitions = model.zodImports
|
|
141
|
-
.flatMap(zodImport => zodImport.types)
|
|
142
|
-
.filter((type, index, array) => array.indexOf(type) === index) // Remove duplicates
|
|
143
|
-
.map(type => {
|
|
144
|
-
// If it ends with 'Schema', create a corresponding type
|
|
145
|
-
if (type.endsWith('Schema')) {
|
|
146
|
-
const typeName = type.replace('Schema', '');
|
|
147
|
-
return `export type ${typeName} = z.infer<typeof ${type}>;`;
|
|
148
|
-
}
|
|
149
|
-
return '';
|
|
150
|
-
})
|
|
151
|
-
.filter(Boolean)
|
|
152
|
-
.join('\n');
|
|
153
|
-
if (customTypeDefinitions) {
|
|
154
|
-
customImports += customTypeDefinitions + '\n\n';
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// If there's any error with zod imports, just use a fallback
|
|
157
|
+
console.warn('Warning: Skipping custom zod imports due to configuration issues');
|
|
158
|
+
customImports = '';
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
// Generate the type content with TSDoc comments
|
|
@@ -264,6 +268,28 @@ ${withRelationsProps
|
|
|
264
268
|
.join(',\n')}
|
|
265
269
|
* }
|
|
266
270
|
* });
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* // OR conditions - find records matching ANY condition
|
|
274
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
275
|
+
* where: {
|
|
276
|
+
* OR: [
|
|
277
|
+
* { name: "John" },
|
|
278
|
+
* { email: { contains: "@admin" } }
|
|
279
|
+
* ]
|
|
280
|
+
* }
|
|
281
|
+
* });
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* // AND conditions - find records matching ALL conditions
|
|
285
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
286
|
+
* where: {
|
|
287
|
+
* AND: [
|
|
288
|
+
* { active: true },
|
|
289
|
+
* { age: { gte: 18 } }
|
|
290
|
+
* ]
|
|
291
|
+
* }
|
|
292
|
+
* });
|
|
267
293
|
*/
|
|
268
294
|
export type ${modelName}WhereInput = {
|
|
269
295
|
${model.fields
|
|
@@ -285,6 +311,12 @@ ${model.fields
|
|
|
285
311
|
}
|
|
286
312
|
return '';
|
|
287
313
|
}).filter(Boolean))
|
|
314
|
+
.concat([
|
|
315
|
+
' /** OR condition - match records that satisfy ANY of the provided conditions */',
|
|
316
|
+
` OR?: ${modelName}WhereInput[];`,
|
|
317
|
+
' /** AND condition - match records that satisfy ALL of the provided conditions */',
|
|
318
|
+
` AND?: ${modelName}WhereInput[];`
|
|
319
|
+
])
|
|
288
320
|
.join('\n')}
|
|
289
321
|
};
|
|
290
322
|
|
|
@@ -655,7 +687,6 @@ ${createInputProps
|
|
|
655
687
|
fs_1.default.mkdirSync(config_1.TYPES_DIR, { recursive: true });
|
|
656
688
|
}
|
|
657
689
|
fs_1.default.writeFileSync(outputPath, typeContent);
|
|
658
|
-
console.log(`Generated type definitions for ${modelName} at ${outputPath}`);
|
|
659
690
|
return {
|
|
660
691
|
modelName,
|
|
661
692
|
tableName,
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,6 @@ function checkEnvironmentVariables() {
|
|
|
84
84
|
errorMessage += '\nPlease add these variables to your .env file or ensure they are available in your environment and try again.';
|
|
85
85
|
throw new Error(errorMessage);
|
|
86
86
|
}
|
|
87
|
-
console.log('✅ All required environment variables are set.');
|
|
88
87
|
}
|
|
89
88
|
function analyzePrismaSchema(schemaPath) {
|
|
90
89
|
try {
|
|
@@ -148,8 +147,6 @@ function analyzePrismaSchema(schemaPath) {
|
|
|
148
147
|
*/
|
|
149
148
|
async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
150
149
|
try {
|
|
151
|
-
// COMPLETELY BYPASS NORMAL OPERATION FOR SIMPLICITY
|
|
152
|
-
console.log('🔧 Using direct SQL approach to avoid PostgreSQL case sensitivity issues...');
|
|
153
150
|
// Load environment variables
|
|
154
151
|
dotenv.config();
|
|
155
152
|
// Get direct PostgreSQL connection URL
|
|
@@ -162,7 +159,6 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
162
159
|
const pg = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
163
160
|
const { Pool } = pg.default || pg;
|
|
164
161
|
const pool = new Pool({ connectionString: process.env.DIRECT_URL });
|
|
165
|
-
console.log('🔌 Connected to PostgreSQL database for configuration.');
|
|
166
162
|
const { rows: allTables } = await pool.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`);
|
|
167
163
|
for (const model of modelInfos) {
|
|
168
164
|
const matchingTable = allTables.find((t) => t.table_name.toLowerCase() === model.tableName.toLowerCase());
|
|
@@ -171,29 +167,20 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
171
167
|
continue;
|
|
172
168
|
}
|
|
173
169
|
const actualTableName = matchingTable.table_name;
|
|
174
|
-
console.log(`Processing model ${model.name} (table: "${actualTableName}")`);
|
|
175
170
|
// Realtime setup (existing logic)
|
|
176
171
|
if (model.enableRealtime) {
|
|
177
172
|
const alterPublicationQuery = `ALTER PUBLICATION supabase_realtime ADD TABLE "${actualTableName}";`;
|
|
178
173
|
try {
|
|
179
174
|
await pool.query(alterPublicationQuery);
|
|
180
|
-
console.log(` ✅ Added "${actualTableName}" to supabase_realtime publication for real-time updates.`);
|
|
181
175
|
}
|
|
182
176
|
catch (err) {
|
|
183
|
-
if (err.message.includes('already member')) {
|
|
184
|
-
console.log(` ℹ️ Table "${actualTableName}" was already in supabase_realtime publication.`);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
177
|
+
if (!err.message.includes('already member')) {
|
|
187
178
|
console.error(` ❌ Failed to add "${actualTableName}" to supabase_realtime: ${err.message}`);
|
|
188
179
|
}
|
|
189
180
|
}
|
|
190
181
|
}
|
|
191
|
-
else {
|
|
192
|
-
console.log(` ℹ️ Realtime disabled for model ${model.name}.`);
|
|
193
|
-
}
|
|
194
182
|
// Search setup
|
|
195
183
|
if (model.searchFields.length > 0) {
|
|
196
|
-
console.log(` 🔍 Setting up full-text search for model ${model.name}:`);
|
|
197
184
|
const { rows: columns } = await pool.query(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [actualTableName]);
|
|
198
185
|
for (const searchField of model.searchFields) {
|
|
199
186
|
const matchingColumn = columns.find((c) => c.column_name.toLowerCase() === searchField.name.toLowerCase());
|
|
@@ -204,7 +191,6 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
204
191
|
const actualColumnName = matchingColumn.column_name;
|
|
205
192
|
const functionName = `search_${actualTableName.toLowerCase()}_by_${actualColumnName.toLowerCase()}_prefix`;
|
|
206
193
|
const indexName = `idx_gin_search_${actualTableName.toLowerCase()}_${actualColumnName.toLowerCase()}`;
|
|
207
|
-
console.log(` ➡️ Configuring field "${actualColumnName}":`);
|
|
208
194
|
try {
|
|
209
195
|
// Create search function
|
|
210
196
|
const createFunctionQuery = `
|
|
@@ -217,7 +203,6 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
217
203
|
END;
|
|
218
204
|
$$ LANGUAGE plpgsql STABLE;`; // Added STABLE for potential performance benefits
|
|
219
205
|
await pool.query(createFunctionQuery);
|
|
220
|
-
console.log(` ✅ Created/Replaced RPC function: "${functionName}"(search_prefix text)`);
|
|
221
206
|
// Create GIN index
|
|
222
207
|
const createIndexQuery = `
|
|
223
208
|
DO $$
|
|
@@ -229,39 +214,21 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
229
214
|
AND indexname = '${indexName}'
|
|
230
215
|
) THEN
|
|
231
216
|
CREATE INDEX "${indexName}" ON "public"."${actualTableName}" USING GIN (to_tsvector('english', "${actualColumnName}"));
|
|
232
|
-
RAISE NOTICE ' ✅ Created GIN index: "${indexName}" on "${actualTableName}"("${actualColumnName}")';
|
|
233
|
-
ELSE
|
|
234
|
-
RAISE NOTICE ' ℹ️ GIN index "${indexName}" on "${actualTableName}"("${actualColumnName}") already exists.';
|
|
235
217
|
END IF;
|
|
236
218
|
END;
|
|
237
219
|
$$;`;
|
|
238
|
-
|
|
239
|
-
// Output notices from the DO $$ block (PostgreSQL specific)
|
|
240
|
-
if (indexResult.rows.length > 0 && indexResult.rows[0].notice) {
|
|
241
|
-
console.log(indexResult.rows[0].notice.replace(/^NOTICE: /, ''));
|
|
242
|
-
}
|
|
243
|
-
else if (!indexResult.rows.find((r) => r.notice?.includes('Created GIN index'))) {
|
|
244
|
-
// If DO $$ block doesn't emit specific notice for creation and it didn't say exists.
|
|
245
|
-
// This is a fallback log, actual creation/existence is handled by the DO block.
|
|
246
|
-
// The important part is that the index will be there.
|
|
247
|
-
}
|
|
220
|
+
await pool.query(createIndexQuery);
|
|
248
221
|
}
|
|
249
222
|
catch (err) {
|
|
250
223
|
console.error(` ❌ Failed to set up search for "${actualTableName}"."${actualColumnName}": ${err.message}`);
|
|
251
224
|
}
|
|
252
225
|
}
|
|
253
226
|
}
|
|
254
|
-
else {
|
|
255
|
-
console.log(` ℹ️ No fields marked with // @enableSearch for model ${model.name}.`);
|
|
256
|
-
}
|
|
257
|
-
console.log('---------------------------------------------------');
|
|
258
227
|
}
|
|
259
228
|
await pool.end();
|
|
260
|
-
console.log('🎉 Database configuration complete.');
|
|
261
229
|
}
|
|
262
230
|
catch (err) {
|
|
263
231
|
console.error('❌ Error during database configuration:', err);
|
|
264
|
-
console.log('⚠️ Hook generation will continue, but database features like search or realtime might not be fully configured.');
|
|
265
232
|
}
|
|
266
233
|
}
|
|
267
234
|
/**
|
|
@@ -269,22 +236,16 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
|
|
|
269
236
|
*/
|
|
270
237
|
async function generateHooks() {
|
|
271
238
|
try {
|
|
272
|
-
console.log('🚀 Starting Suparisma hook generation...');
|
|
273
239
|
checkEnvironmentVariables();
|
|
274
|
-
console.log(`Prisma schema path: ${config_1.PRISMA_SCHEMA_PATH}`);
|
|
275
|
-
console.log(`Output directory: ${config_1.OUTPUT_DIR}`);
|
|
276
240
|
// Delete the entire output directory if it exists to clean up any stale files
|
|
277
241
|
if (fs_1.default.existsSync(config_1.OUTPUT_DIR)) {
|
|
278
|
-
console.log(`🧹 Cleaning up previous generated files in ${config_1.OUTPUT_DIR}...`);
|
|
279
242
|
fs_1.default.rmSync(config_1.OUTPUT_DIR, { recursive: true, force: true });
|
|
280
|
-
console.log(`✅ Removed previous generated directory`);
|
|
281
243
|
}
|
|
282
244
|
// Ensure all specific output directories exist, OUTPUT_DIR is the root and will be created if needed by sub-creations.
|
|
283
245
|
const dirsToEnsure = [config_1.TYPES_DIR, config_1.HOOKS_DIR, config_1.UTILS_DIR];
|
|
284
246
|
dirsToEnsure.forEach(dir => {
|
|
285
247
|
if (!fs_1.default.existsSync(dir)) {
|
|
286
248
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
287
|
-
console.log(`Created directory: ${dir}`);
|
|
288
249
|
}
|
|
289
250
|
});
|
|
290
251
|
// Generate Supabase client file (goes to UTILS_DIR)
|
|
@@ -300,7 +261,6 @@ async function generateHooks() {
|
|
|
300
261
|
modelInfos.push(modelInfo);
|
|
301
262
|
}
|
|
302
263
|
(0, indexGenerator_1.generateMainIndexFile)(modelInfos);
|
|
303
|
-
console.log(`✅ Successfully generated all suparisma hooks and types in "${config_1.OUTPUT_DIR}"!`);
|
|
304
264
|
}
|
|
305
265
|
catch (error) {
|
|
306
266
|
console.error('❌ Error generating hooks:', error);
|
package/package.json
CHANGED