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 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
- console.log('Created!', result);
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
- const arrayValue = JSON.stringify(advancedOps.has);
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
- const arrayValue = JSON.stringify(advancedOps.hasEvery);
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
- const arrayValue = JSON.stringify(advancedOps.hasSome);
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
- return filters.length > 0 ? filters.join(',') : undefined;
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
- * @example
492
- * // With filtering
493
- * const users = useSuparismaUser({
494
- * where: { role: 'admin' },
495
- * orderBy: { created_at: 'desc' }
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
- 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;
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
- 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
- }
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
- 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
- }
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
- 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;
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((status) => {
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
- 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
- }
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
  }
@@ -101,5 +101,4 @@ 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}`);
105
104
  }
@@ -22,5 +22,4 @@ 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}`);
26
25
  }
@@ -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
- // 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}'`;
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
- // 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';
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
- 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
- }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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": {