suparisma 1.0.8 → 1.0.12

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:
@@ -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,112 +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
- console.log(\`Filter mismatch on \${key}\`, { expected: value, actual: record[key as keyof typeof record] });
715
- return false;
716
- }
717
- }
718
- }
719
-
720
- return true;
721
- }
722
-
723
448
  /**
724
449
  * Core hook factory function that creates a type-safe realtime hook for a specific model.
725
450
  * This is the foundation for all Suparisma hooks.
@@ -763,23 +488,12 @@ export function createSuparismaHook<
763
488
  * const users = useSuparismaUser();
764
489
  * const { data, loading, error } = users;
765
490
  *
766
- * @example
767
- * // With filtering
768
- * const users = useSuparismaUser({
769
- * where: { role: 'admin' },
770
- * orderBy: { created_at: 'desc' }
771
- * });
772
- *
773
- * @example
774
- * // With OR conditions for search
775
- * const users = useSuparismaUser({
776
- * where: {
777
- * OR: [
778
- * { name: { contains: "john" } },
779
- * { email: { contains: "john" } }
780
- * ]
781
- * }
782
- * });
491
+ * @example
492
+ * // With filtering
493
+ * const users = useSuparismaUser({
494
+ * where: { role: 'admin' },
495
+ * orderBy: { created_at: 'desc' }
496
+ * });
783
497
  */
784
498
  return function useSuparismaHook(options: SuparismaOptions<TWhereInput, TOrderByInput> = {}) {
785
499
  const {
@@ -990,7 +704,19 @@ export function createSuparismaHook<
990
704
 
991
705
  // Apply any where conditions client-side
992
706
  if (where) {
993
- 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
+ });
994
720
  }
995
721
 
996
722
  // Set count directly for search results
@@ -1186,33 +912,19 @@ export function createSuparismaHook<
1186
912
 
1187
913
  const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
1188
914
 
1189
- // 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
1190
916
  let hasComplexArrayFilters = false;
1191
917
  if (where) {
1192
- const checkForComplexFilters = (whereClause: any): boolean => {
1193
- for (const [key, value] of Object.entries(whereClause)) {
1194
- // OR/AND conditions require client-side filtering
1195
- if (key === 'OR' || key === 'AND') {
1196
- return true;
1197
- }
1198
-
1199
- if (typeof value === 'object' && value !== null) {
1200
- // Check if it's an array of conditions (nested OR/AND)
1201
- if (Array.isArray(value)) {
1202
- return true;
1203
- }
1204
-
1205
- const advancedOps = value as any;
1206
- // Check for complex array operators
1207
- if ('has' in advancedOps || 'hasEvery' in advancedOps || 'hasSome' in advancedOps || 'isEmpty' in advancedOps) {
1208
- return true;
1209
- }
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;
1210
925
  }
1211
926
  }
1212
- return false;
1213
- };
1214
-
1215
- hasComplexArrayFilters = checkForComplexFilters(where);
927
+ }
1216
928
  }
1217
929
 
1218
930
  // For complex array filters, use no database filter and rely on client-side filtering
@@ -1269,7 +981,49 @@ export function createSuparismaHook<
1269
981
  // ALWAYS check if this record matches our filter client-side
1270
982
  // This is especially important for complex array filters
1271
983
  if (currentWhere) { // Use ref value
1272
- 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
+ }
1273
1027
 
1274
1028
  if (!matchesFilter) {
1275
1029
  console.log('New record does not match filter criteria, skipping');
@@ -1355,7 +1109,46 @@ export function createSuparismaHook<
1355
1109
 
1356
1110
  // Check if the updated record still matches our current filter
1357
1111
  if (currentWhere) {
1358
- 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
+ }
1359
1152
 
1360
1153
  // If the updated record doesn't match the filter, remove it from the list
1361
1154
  if (!matchesFilter) {
@@ -2081,7 +1874,6 @@ export function createSuparismaHook<
2081
1874
  : api;
2082
1875
  };
2083
1876
  }
2084
-
2085
1877
  `; // Ensure template literal is closed
2086
1878
  // Output to the UTILS_DIR
2087
1879
  const outputPath = path_1.default.join(config_1.UTILS_DIR, 'core.ts');
@@ -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
 
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.8",
3
+ "version": "1.0.12",
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": {