suparisma 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,6 @@ 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)
27
26
  - [Sorting Data](#sorting-data)
28
27
  - [Pagination](#pagination)
29
28
  - [Search Functionality](#search-functionality)
@@ -62,7 +61,6 @@ Suparisma bridges this gap by:
62
61
  - 🔄 **Real-time updates by default** for all tables (with opt-out capability)
63
62
  - 🔒 **Type-safe interfaces** for all database operations
64
63
  - 🔍 **Full-text search** with configurable annotations *(currently under maintenance)*
65
- - ⚡ **Advanced filtering** with OR/AND conditions for complex queries
66
64
  - 🔢 **Pagination and sorting** built into every hook
67
65
  - 🧩 **Prisma-like API** that feels familiar if you already use Prisma
68
66
  - 📱 **Works with any React framework** including Next.js, Remix, etc.
@@ -582,215 +580,6 @@ function ProductFilter() {
582
580
  }
583
581
  ```
584
582
 
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
-
794
583
  ### Sorting Data
795
584
 
796
585
  Sort data using Prisma-like ordering:
@@ -1166,7 +955,7 @@ async function handleSubmit(event) {
1166
955
  name: formData.name,
1167
956
  someNumber: parseInt(formData.number)
1168
957
  });
1169
- // Handle successful creation
958
+ console.log('Created!', result);
1170
959
  } catch (err) {
1171
960
  console.error('Failed to create thing:', err);
1172
961
  }
@@ -94,21 +94,6 @@ 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>[];
112
97
  };
113
98
 
114
99
  /**
@@ -192,19 +177,6 @@ export type SearchState = {
192
177
  clearQueries: () => void;
193
178
  };
194
179
 
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
-
208
180
  /**
209
181
  * Compare two values for sorting with proper type handling
210
182
  */
@@ -243,73 +215,9 @@ export function buildFilterString<T>(where?: T): string | undefined {
243
215
  if (!where) return undefined;
244
216
 
245
217
  const filters: string[] = [];
246
- const orConditions: string[] = [];
247
218
 
248
219
  for (const [key, value] of Object.entries(where)) {
249
220
  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
-
313
221
  if (typeof value === 'object' && value !== null) {
314
222
  // Handle advanced operators
315
223
  const advancedOps = value as unknown as FilterOperators<any>;
@@ -357,22 +265,19 @@ export function buildFilterString<T>(where?: T): string | undefined {
357
265
  // Array-specific operators
358
266
  if ('has' in advancedOps && advancedOps.has !== undefined) {
359
267
  // Array contains ANY of the specified items (overlaps)
360
- // Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
361
- const arrayValue = \`{\${advancedOps.has.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
268
+ const arrayValue = JSON.stringify(advancedOps.has);
362
269
  filters.push(\`\${key}=ov.\${arrayValue}\`);
363
270
  }
364
271
 
365
272
  if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
366
273
  // Array contains ALL of the specified items (contains)
367
- // Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
368
- const arrayValue = \`{\${advancedOps.hasEvery.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
274
+ const arrayValue = JSON.stringify(advancedOps.hasEvery);
369
275
  filters.push(\`\${key}=cs.\${arrayValue}\`);
370
276
  }
371
277
 
372
278
  if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
373
279
  // Array contains ANY of the specified items (overlaps)
374
- // Use PostgreSQL array syntax: {item1,item2} not ["item1","item2"]
375
- const arrayValue = \`{\${advancedOps.hasSome.map((item: any) => escapeFilterValue(item)).join(',')}}\`;
280
+ const arrayValue = JSON.stringify(advancedOps.hasSome);
376
281
  filters.push(\`\${key}=ov.\${arrayValue}\`);
377
282
  }
378
283
 
@@ -392,18 +297,7 @@ export function buildFilterString<T>(where?: T): string | undefined {
392
297
  }
393
298
  }
394
299
 
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;
300
+ return filters.length > 0 ? filters.join(',') : undefined;
407
301
  }
408
302
 
409
303
  /**
@@ -420,69 +314,6 @@ export function applyFilter<T>(
420
314
  // Apply each filter condition
421
315
  for (const [key, value] of Object.entries(where)) {
422
316
  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
-
486
317
  if (typeof value === 'object' && value !== null) {
487
318
  // Handle advanced operators
488
319
  const advancedOps = value as unknown as FilterOperators<any>;
@@ -614,111 +445,6 @@ export function applyOrderBy<T>(
614
445
  return orderedQuery;
615
446
  }
616
447
 
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
-
722
448
  /**
723
449
  * Core hook factory function that creates a type-safe realtime hook for a specific model.
724
450
  * This is the foundation for all Suparisma hooks.
@@ -762,23 +488,12 @@ export function createSuparismaHook<
762
488
  * const users = useSuparismaUser();
763
489
  * const { data, loading, error } = users;
764
490
  *
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
- * });
491
+ * @example
492
+ * // With filtering
493
+ * const users = useSuparismaUser({
494
+ * where: { role: 'admin' },
495
+ * orderBy: { created_at: 'desc' }
496
+ * });
782
497
  */
783
498
  return function useSuparismaHook(options: SuparismaOptions<TWhereInput, TOrderByInput> = {}) {
784
499
  const {
@@ -989,7 +704,19 @@ export function createSuparismaHook<
989
704
 
990
705
  // Apply any where conditions client-side
991
706
  if (where) {
992
- results = results.filter((item) => clientSideFilterCheck(item, 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
+ });
993
720
  }
994
721
 
995
722
  // Set count directly for search results
@@ -1185,33 +912,19 @@ export function createSuparismaHook<
1185
912
 
1186
913
  const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
1187
914
 
1188
- // Check if we have complex array filters or OR/AND conditions that should be handled client-side only
915
+ // Check if we have complex array filters that should be handled client-side only
1189
916
  let hasComplexArrayFilters = false;
1190
917
  if (where) {
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
- }
918
+ for (const [key, value] of Object.entries(where)) {
919
+ if (typeof value === 'object' && value !== null) {
920
+ const advancedOps = value as any;
921
+ // Check for complex array operators
922
+ if ('has' in advancedOps || 'hasEvery' in advancedOps || 'hasSome' in advancedOps || 'isEmpty' in advancedOps) {
923
+ hasComplexArrayFilters = true;
924
+ break;
1209
925
  }
1210
926
  }
1211
- return false;
1212
- };
1213
-
1214
- hasComplexArrayFilters = checkForComplexFilters(where);
927
+ }
1215
928
  }
1216
929
 
1217
930
  // For complex array filters, use no database filter and rely on client-side filtering
@@ -1224,15 +937,18 @@ export function createSuparismaHook<
1224
937
 
1225
938
  if (hasComplexArrayFilters) {
1226
939
  // 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\`);
1227
941
  } else if (where) {
1228
942
  // Include filter for simple operations
1229
943
  const filter = buildFilterString(where);
1230
944
  if (filter) {
1231
945
  subscriptionConfig.filter = filter;
1232
946
  }
947
+ console.log(\`Setting up subscription for \${tableName} with database filter: \${filter}\`);
1233
948
  } else if (realtimeFilter) {
1234
949
  // Use custom realtime filter if provided
1235
950
  subscriptionConfig.filter = realtimeFilter;
951
+ console.log(\`Setting up subscription for \${tableName} with custom filter: \${realtimeFilter}\`);
1236
952
  }
1237
953
 
1238
954
  const channel = supabase
@@ -1241,6 +957,7 @@ export function createSuparismaHook<
1241
957
  'postgres_changes',
1242
958
  subscriptionConfig,
1243
959
  (payload) => {
960
+ console.log(\`🔥 REALTIME EVENT RECEIVED for \${tableName}:\`, payload.eventType, payload);
1244
961
 
1245
962
  // Access current options via refs inside the event handler
1246
963
  const currentWhere = whereRef.current;
@@ -1250,6 +967,7 @@ export function createSuparismaHook<
1250
967
 
1251
968
  // Skip realtime updates when search is active
1252
969
  if (isSearchingRef.current) {
970
+ console.log('⏭️ Skipping realtime update - search is active');
1253
971
  return;
1254
972
  }
1255
973
 
@@ -1258,13 +976,57 @@ export function createSuparismaHook<
1258
976
  setData((prev) => {
1259
977
  try {
1260
978
  const newRecord = payload.new as TWithRelations;
979
+ console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
1261
980
 
1262
981
  // ALWAYS check if this record matches our filter client-side
1263
982
  // This is especially important for complex array filters
1264
983
  if (currentWhere) { // Use ref value
1265
- const matchesFilter = clientSideFilterCheck(newRecord, currentWhere);
984
+ let matchesFilter = true;
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
+ }
1266
1027
 
1267
1028
  if (!matchesFilter) {
1029
+ console.log('New record does not match filter criteria, skipping');
1268
1030
  return prev;
1269
1031
  }
1270
1032
  }
@@ -1276,6 +1038,7 @@ export function createSuparismaHook<
1276
1038
  );
1277
1039
 
1278
1040
  if (exists) {
1041
+ console.log('Record already exists, skipping insertion');
1279
1042
  return prev;
1280
1043
  }
1281
1044
 
@@ -1346,10 +1109,50 @@ export function createSuparismaHook<
1346
1109
 
1347
1110
  // Check if the updated record still matches our current filter
1348
1111
  if (currentWhere) {
1349
- const matchesFilter = clientSideFilterCheck(updatedRecord, currentWhere);
1112
+ let matchesFilter = true;
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
+ }
1350
1152
 
1351
1153
  // If the updated record doesn't match the filter, remove it from the list
1352
1154
  if (!matchesFilter) {
1155
+ console.log('Updated record no longer matches filter, removing from list');
1353
1156
  return prev.filter((item) =>
1354
1157
  // @ts-ignore: Supabase typing issue
1355
1158
  !('id' in item && 'id' in updatedRecord && item.id === updatedRecord.id)
@@ -1408,7 +1211,9 @@ export function createSuparismaHook<
1408
1211
  });
1409
1212
  } else if (payload.eventType === 'DELETE') {
1410
1213
  // Process delete event
1214
+ console.log('🗑️ Processing DELETE event for', tableName);
1411
1215
  setData((prev) => {
1216
+ console.log('🗑️ DELETE: Current data before deletion:', prev.length, 'items');
1412
1217
 
1413
1218
  // Access current options via refs
1414
1219
  const currentWhere = whereRef.current;
@@ -1418,6 +1223,7 @@ export function createSuparismaHook<
1418
1223
 
1419
1224
  // Skip if search is active
1420
1225
  if (isSearchingRef.current) {
1226
+ console.log('⏭️ DELETE: Skipping - search is active');
1421
1227
  return prev;
1422
1228
  }
1423
1229
 
@@ -1427,14 +1233,21 @@ export function createSuparismaHook<
1427
1233
  // Filter out the deleted item
1428
1234
  const filteredData = prev.filter((item) => {
1429
1235
  // @ts-ignore: Supabase typing issue
1430
- return !('id' in item && 'id' in payload.old && item.id === payload.old.id);
1236
+ const shouldKeep = !('id' in item && 'id' in payload.old && item.id === payload.old.id);
1237
+ if (!shouldKeep) {
1238
+ console.log('🗑️ DELETE: Removing item with ID:', item.id);
1239
+ }
1240
+ return shouldKeep;
1431
1241
  });
1432
1242
 
1243
+ console.log('🗑️ DELETE: Data after deletion:', filteredData.length, 'items (was', currentSize, ')');
1244
+
1433
1245
  // Fetch the updated count after the data changes
1434
1246
  setTimeout(() => fetchTotalCount(), 0);
1435
1247
 
1436
1248
  // If we need to maintain the size with a limit
1437
1249
  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\`);
1438
1251
 
1439
1252
  // Use setTimeout to ensure this state update completes first
1440
1253
  setTimeout(() => {
@@ -1492,16 +1305,19 @@ export function createSuparismaHook<
1492
1305
  }
1493
1306
  }
1494
1307
  )
1495
- .subscribe();
1308
+ .subscribe((status) => {
1309
+ console.log(\`Subscription status for \${tableName}\`, status);
1310
+ });
1496
1311
 
1497
1312
  // Store the channel ref
1498
1313
  channelRef.current = channel;
1499
1314
 
1500
- return () => {
1501
- if (channelRef.current) {
1502
- supabase.removeChannel(channelRef.current); // Correct way to remove channel
1503
- channelRef.current = null;
1504
- }
1315
+ return () => {
1316
+ console.log(\`Unsubscribing from \${channelId}\`);
1317
+ if (channelRef.current) {
1318
+ supabase.removeChannel(channelRef.current); // Correct way to remove channel
1319
+ channelRef.current = null;
1320
+ }
1505
1321
 
1506
1322
  if (searchTimeoutRef.current) {
1507
1323
  clearTimeout(searchTimeoutRef.current);
@@ -1546,6 +1362,7 @@ export function createSuparismaHook<
1546
1362
  if (initialLoadRef.current) {
1547
1363
  // Only reload if options have changed significantly
1548
1364
  if (optionsChanged()) {
1365
+ console.log(\`Options changed for \${tableName}, reloading data\`);
1549
1366
  findMany({
1550
1367
  where,
1551
1368
  orderBy,
@@ -1575,6 +1392,8 @@ export function createSuparismaHook<
1575
1392
  /**
1576
1393
  * Create a new record with the provided data.
1577
1394
  * Default values from the schema will be applied if not provided.
1395
+ * NOTE: This operation does NOT immediately update the local state.
1396
+ * The state will be updated when the realtime INSERT event is received.
1578
1397
  *
1579
1398
  * @param data - The data to create the record with
1580
1399
  * @returns A promise with the created record or error
@@ -1647,8 +1466,8 @@ export function createSuparismaHook<
1647
1466
 
1648
1467
  if (error) throw error;
1649
1468
 
1650
- // Update the total count after a successful creation
1651
- setTimeout(() => fetchTotalCount(), 0);
1469
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime INSERT event handle it
1470
+ console.log('✅ Created ' + tableName + ' record, waiting for realtime INSERT event to update state');
1652
1471
 
1653
1472
  // Return created record
1654
1473
  return { data: result?.[0] as TWithRelations, error: null };
@@ -1659,10 +1478,12 @@ export function createSuparismaHook<
1659
1478
  } finally {
1660
1479
  setLoading(false);
1661
1480
  }
1662
- }, [fetchTotalCount]);
1481
+ }, []);
1663
1482
 
1664
1483
  /**
1665
1484
  * Update an existing record identified by a unique identifier.
1485
+ * NOTE: This operation does NOT immediately update the local state.
1486
+ * The state will be updated when the realtime UPDATE event is received.
1666
1487
  *
1667
1488
  * @param params - Object containing the identifier and update data
1668
1489
  * @returns A promise with the updated record or error
@@ -1724,10 +1545,8 @@ export function createSuparismaHook<
1724
1545
 
1725
1546
  if (error) throw error;
1726
1547
 
1727
- // Update the total count after a successful update
1728
- // This is for consistency with other operations, and because
1729
- // updates can sometimes affect filtering based on updated values
1730
- setTimeout(() => fetchTotalCount(), 0);
1548
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime UPDATE event handle it
1549
+ console.log('✅ Updated ' + tableName + ' record, waiting for realtime UPDATE event to update state');
1731
1550
 
1732
1551
  // Return updated record
1733
1552
  return { data: data?.[0] as TWithRelations, error: null };
@@ -1738,10 +1557,12 @@ export function createSuparismaHook<
1738
1557
  } finally {
1739
1558
  setLoading(false);
1740
1559
  }
1741
- }, [fetchTotalCount]);
1560
+ }, []);
1742
1561
 
1743
1562
  /**
1744
1563
  * Delete a record by its unique identifier.
1564
+ * NOTE: This operation does NOT immediately update the local state.
1565
+ * The state will be updated when the realtime DELETE event is received.
1745
1566
  *
1746
1567
  * @param where - The unique identifier to delete the record by
1747
1568
  * @returns A promise with the deleted record or error
@@ -1791,8 +1612,8 @@ export function createSuparismaHook<
1791
1612
 
1792
1613
  if (error) throw error;
1793
1614
 
1794
- // Update the total count after a successful deletion
1795
- setTimeout(() => fetchTotalCount(), 0);
1615
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE event handle it
1616
+ console.log('✅ Deleted ' + tableName + ' record, waiting for realtime DELETE event to update state');
1796
1617
 
1797
1618
  // Return the deleted record
1798
1619
  return { data: recordToDelete as TWithRelations, error: null };
@@ -1803,10 +1624,12 @@ export function createSuparismaHook<
1803
1624
  } finally {
1804
1625
  setLoading(false);
1805
1626
  }
1806
- }, [fetchTotalCount]);
1627
+ }, []);
1807
1628
 
1808
1629
  /**
1809
1630
  * Delete multiple records matching the filter criteria.
1631
+ * NOTE: This operation does NOT immediately update the local state.
1632
+ * The state will be updated when realtime DELETE events are received for each record.
1810
1633
  *
1811
1634
  * @param params - Query parameters for filtering records to delete
1812
1635
  * @returns A promise with the count of deleted records or error
@@ -1816,7 +1639,7 @@ export function createSuparismaHook<
1816
1639
  * const result = await users.deleteMany({
1817
1640
  * where: { active: false }
1818
1641
  * });
1819
- * console.log(\`Deleted \${result.count} inactive users\`);
1642
+ * console.log('Deleted ' + result.count + ' inactive users');
1820
1643
  *
1821
1644
  * @example
1822
1645
  * // Delete all records (use with caution!)
@@ -1860,8 +1683,8 @@ export function createSuparismaHook<
1860
1683
 
1861
1684
  if (deleteError) throw deleteError;
1862
1685
 
1863
- // Update the total count after a successful bulk deletion
1864
- setTimeout(() => fetchTotalCount(), 0);
1686
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE events handle it
1687
+ console.log('✅ Deleted ' + recordsToDelete.length + ' ' + tableName + ' records, waiting for realtime DELETE events to update state');
1865
1688
 
1866
1689
  // Return the count of deleted records
1867
1690
  return { count: recordsToDelete.length, error: null };
@@ -1872,7 +1695,7 @@ export function createSuparismaHook<
1872
1695
  } finally {
1873
1696
  setLoading(false);
1874
1697
  }
1875
- }, [fetchTotalCount]);
1698
+ }, []);
1876
1699
 
1877
1700
  /**
1878
1701
  * Find the first record matching the filter criteria.
@@ -2057,7 +1880,6 @@ export function createSuparismaHook<
2057
1880
  : api;
2058
1881
  };
2059
1882
  }
2060
-
2061
1883
  `; // Ensure template literal is closed
2062
1884
  // Output to the UTILS_DIR
2063
1885
  const outputPath = path_1.default.join(config_1.UTILS_DIR, 'core.ts');
@@ -2065,4 +1887,5 @@ export function createSuparismaHook<
2065
1887
  fs_1.default.mkdirSync(config_1.UTILS_DIR, { recursive: true });
2066
1888
  }
2067
1889
  fs_1.default.writeFileSync(outputPath, coreContent);
1890
+ console.log(`Generated core utility file at ${outputPath}`);
2068
1891
  }
@@ -113,4 +113,5 @@ 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}`);
116
117
  }
@@ -101,4 +101,5 @@ export default useSuparisma;
101
101
  fs_1.default.mkdirSync(config_1.OUTPUT_DIR, { recursive: true });
102
102
  }
103
103
  fs_1.default.writeFileSync(outputPath, content);
104
+ console.log(`Generated main module file at ${outputPath}`);
104
105
  }
@@ -22,4 +22,5 @@ export const supabase = createClient(
22
22
  fs_1.default.mkdirSync(config_1.UTILS_DIR, { recursive: true });
23
23
  }
24
24
  fs_1.default.writeFileSync(outputPath, supabaseClientContent);
25
+ console.log(`🚀 Generated Supabase client file at: ${outputPath}`);
25
26
  }
@@ -118,44 +118,40 @@ 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
- 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';
121
+ // Add zod import for type inference
122
+ customImports = 'import { z } from \'zod\';\n';
123
+ // Get the zod schemas file path from environment variable
124
+ const zodSchemasPath = process.env.ZOD_SCHEMAS_FILE_PATH || '../commonTypes';
125
+ // Add custom imports with environment variable path
126
+ customImports += model.zodImports
127
+ .map(zodImport => {
128
+ // Extract the types from the original import statement
129
+ const typeMatch = zodImport.importStatement.match(/import\s+{\s*([^}]+)\s*}\s+from/);
130
+ if (typeMatch) {
131
+ const types = typeMatch[1].trim();
132
+ return `import { ${types} } from '${zodSchemasPath}'`;
153
133
  }
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 = '';
134
+ // Fallback to original import if parsing fails
135
+ return zodImport.importStatement;
136
+ })
137
+ .join('\n') + '\n\n';
138
+ // Add type definitions for imported zod schemas if needed
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';
159
155
  }
160
156
  }
161
157
  // Generate the type content with TSDoc comments
@@ -268,28 +264,6 @@ ${withRelationsProps
268
264
  .join(',\n')}
269
265
  * }
270
266
  * });
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
- * });
293
267
  */
294
268
  export type ${modelName}WhereInput = {
295
269
  ${model.fields
@@ -311,12 +285,6 @@ ${model.fields
311
285
  }
312
286
  return '';
313
287
  }).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
- ])
320
288
  .join('\n')}
321
289
  };
322
290
 
@@ -687,6 +655,7 @@ ${createInputProps
687
655
  fs_1.default.mkdirSync(config_1.TYPES_DIR, { recursive: true });
688
656
  }
689
657
  fs_1.default.writeFileSync(outputPath, typeContent);
658
+ console.log(`Generated type definitions for ${modelName} at ${outputPath}`);
690
659
  return {
691
660
  modelName,
692
661
  tableName,
package/dist/index.js CHANGED
@@ -84,6 +84,7 @@ 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.');
87
88
  }
88
89
  function analyzePrismaSchema(schemaPath) {
89
90
  try {
@@ -147,6 +148,8 @@ function analyzePrismaSchema(schemaPath) {
147
148
  */
148
149
  async function configurePrismaTablesForSuparisma(schemaPath) {
149
150
  try {
151
+ // COMPLETELY BYPASS NORMAL OPERATION FOR SIMPLICITY
152
+ console.log('🔧 Using direct SQL approach to avoid PostgreSQL case sensitivity issues...');
150
153
  // Load environment variables
151
154
  dotenv.config();
152
155
  // Get direct PostgreSQL connection URL
@@ -159,6 +162,7 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
159
162
  const pg = await Promise.resolve().then(() => __importStar(require('pg')));
160
163
  const { Pool } = pg.default || pg;
161
164
  const pool = new Pool({ connectionString: process.env.DIRECT_URL });
165
+ console.log('🔌 Connected to PostgreSQL database for configuration.');
162
166
  const { rows: allTables } = await pool.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`);
163
167
  for (const model of modelInfos) {
164
168
  const matchingTable = allTables.find((t) => t.table_name.toLowerCase() === model.tableName.toLowerCase());
@@ -167,20 +171,29 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
167
171
  continue;
168
172
  }
169
173
  const actualTableName = matchingTable.table_name;
174
+ console.log(`Processing model ${model.name} (table: "${actualTableName}")`);
170
175
  // Realtime setup (existing logic)
171
176
  if (model.enableRealtime) {
172
177
  const alterPublicationQuery = `ALTER PUBLICATION supabase_realtime ADD TABLE "${actualTableName}";`;
173
178
  try {
174
179
  await pool.query(alterPublicationQuery);
180
+ console.log(` ✅ Added "${actualTableName}" to supabase_realtime publication for real-time updates.`);
175
181
  }
176
182
  catch (err) {
177
- if (!err.message.includes('already member')) {
183
+ if (err.message.includes('already member')) {
184
+ console.log(` ℹ️ Table "${actualTableName}" was already in supabase_realtime publication.`);
185
+ }
186
+ else {
178
187
  console.error(` ❌ Failed to add "${actualTableName}" to supabase_realtime: ${err.message}`);
179
188
  }
180
189
  }
181
190
  }
191
+ else {
192
+ console.log(` ℹ️ Realtime disabled for model ${model.name}.`);
193
+ }
182
194
  // Search setup
183
195
  if (model.searchFields.length > 0) {
196
+ console.log(` 🔍 Setting up full-text search for model ${model.name}:`);
184
197
  const { rows: columns } = await pool.query(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [actualTableName]);
185
198
  for (const searchField of model.searchFields) {
186
199
  const matchingColumn = columns.find((c) => c.column_name.toLowerCase() === searchField.name.toLowerCase());
@@ -191,6 +204,7 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
191
204
  const actualColumnName = matchingColumn.column_name;
192
205
  const functionName = `search_${actualTableName.toLowerCase()}_by_${actualColumnName.toLowerCase()}_prefix`;
193
206
  const indexName = `idx_gin_search_${actualTableName.toLowerCase()}_${actualColumnName.toLowerCase()}`;
207
+ console.log(` ➡️ Configuring field "${actualColumnName}":`);
194
208
  try {
195
209
  // Create search function
196
210
  const createFunctionQuery = `
@@ -203,6 +217,7 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
203
217
  END;
204
218
  $$ LANGUAGE plpgsql STABLE;`; // Added STABLE for potential performance benefits
205
219
  await pool.query(createFunctionQuery);
220
+ console.log(` ✅ Created/Replaced RPC function: "${functionName}"(search_prefix text)`);
206
221
  // Create GIN index
207
222
  const createIndexQuery = `
208
223
  DO $$
@@ -214,21 +229,39 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
214
229
  AND indexname = '${indexName}'
215
230
  ) THEN
216
231
  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.';
217
235
  END IF;
218
236
  END;
219
237
  $$;`;
220
- await pool.query(createIndexQuery);
238
+ const indexResult = await pool.query(createIndexQuery);
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
+ }
221
248
  }
222
249
  catch (err) {
223
250
  console.error(` ❌ Failed to set up search for "${actualTableName}"."${actualColumnName}": ${err.message}`);
224
251
  }
225
252
  }
226
253
  }
254
+ else {
255
+ console.log(` ℹ️ No fields marked with // @enableSearch for model ${model.name}.`);
256
+ }
257
+ console.log('---------------------------------------------------');
227
258
  }
228
259
  await pool.end();
260
+ console.log('🎉 Database configuration complete.');
229
261
  }
230
262
  catch (err) {
231
263
  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.');
232
265
  }
233
266
  }
234
267
  /**
@@ -236,16 +269,22 @@ async function configurePrismaTablesForSuparisma(schemaPath) {
236
269
  */
237
270
  async function generateHooks() {
238
271
  try {
272
+ console.log('🚀 Starting Suparisma hook generation...');
239
273
  checkEnvironmentVariables();
274
+ console.log(`Prisma schema path: ${config_1.PRISMA_SCHEMA_PATH}`);
275
+ console.log(`Output directory: ${config_1.OUTPUT_DIR}`);
240
276
  // Delete the entire output directory if it exists to clean up any stale files
241
277
  if (fs_1.default.existsSync(config_1.OUTPUT_DIR)) {
278
+ console.log(`🧹 Cleaning up previous generated files in ${config_1.OUTPUT_DIR}...`);
242
279
  fs_1.default.rmSync(config_1.OUTPUT_DIR, { recursive: true, force: true });
280
+ console.log(`✅ Removed previous generated directory`);
243
281
  }
244
282
  // Ensure all specific output directories exist, OUTPUT_DIR is the root and will be created if needed by sub-creations.
245
283
  const dirsToEnsure = [config_1.TYPES_DIR, config_1.HOOKS_DIR, config_1.UTILS_DIR];
246
284
  dirsToEnsure.forEach(dir => {
247
285
  if (!fs_1.default.existsSync(dir)) {
248
286
  fs_1.default.mkdirSync(dir, { recursive: true });
287
+ console.log(`Created directory: ${dir}`);
249
288
  }
250
289
  });
251
290
  // Generate Supabase client file (goes to UTILS_DIR)
@@ -261,6 +300,7 @@ async function generateHooks() {
261
300
  modelInfos.push(modelInfo);
262
301
  }
263
302
  (0, indexGenerator_1.generateMainIndexFile)(modelInfos);
303
+ console.log(`✅ Successfully generated all suparisma hooks and types in "${config_1.OUTPUT_DIR}"!`);
264
304
  }
265
305
  catch (error) {
266
306
  console.error('❌ Error generating hooks:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
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
6
  "repository": {