suparisma 1.1.0 → 1.1.2

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.
@@ -91,9 +91,14 @@ export type FilterOperators<T> = {
91
91
  isEmpty?: T extends Array<any> ? boolean : never;
92
92
  };
93
93
 
94
- // Type for a single field in an advanced where filter
94
+ // Type for a single field in an advanced where filter with OR/AND support
95
95
  export type AdvancedWhereInput<T> = {
96
96
  [K in keyof T]?: T[K] | FilterOperators<T[K]>;
97
+ } & {
98
+ /** Match ANY of the provided conditions */
99
+ OR?: AdvancedWhereInput<T>[];
100
+ /** Match ALL of the provided conditions */
101
+ AND?: AdvancedWhereInput<T>[];
97
102
  };
98
103
 
99
104
  /**
@@ -210,14 +215,26 @@ function compareValues(a: any, b: any, direction: 'asc' | 'desc'): number {
210
215
 
211
216
  /**
212
217
  * Convert a type-safe where filter to Supabase filter string
218
+ * Note: Complex OR/AND operations may not be fully supported in realtime filters
219
+ * and will fall back to client-side filtering
213
220
  */
214
221
  export function buildFilterString<T>(where?: T): string | undefined {
215
222
  if (!where) return undefined;
216
223
 
224
+ const whereObj = where as any;
225
+
226
+ // Check for OR/AND operations - these are complex for realtime filters
227
+ if (whereObj.OR || whereObj.AND) {
228
+ console.log('⚠️ Complex OR/AND filters detected - realtime will use client-side filtering');
229
+ // For complex logical operations, we'll rely on client-side filtering
230
+ // Return undefined to indicate no database-level filter should be applied
231
+ return undefined;
232
+ }
233
+
217
234
  const filters: string[] = [];
218
235
 
219
- for (const [key, value] of Object.entries(where)) {
220
- if (value !== undefined) {
236
+ for (const [key, value] of Object.entries(whereObj)) {
237
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
221
238
  if (typeof value === 'object' && value !== null) {
222
239
  // Handle advanced operators
223
240
  const advancedOps = value as unknown as FilterOperators<any>;
@@ -231,19 +248,23 @@ export function buildFilterString<T>(where?: T): string | undefined {
231
248
  }
232
249
 
233
250
  if ('gt' in advancedOps && advancedOps.gt !== undefined) {
234
- filters.push(\`\${key}=gt.\${advancedOps.gt}\`);
251
+ const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
252
+ filters.push(\`\${key}=gt.\${value}\`);
235
253
  }
236
254
 
237
255
  if ('gte' in advancedOps && advancedOps.gte !== undefined) {
238
- filters.push(\`\${key}=gte.\${advancedOps.gte}\`);
256
+ const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
257
+ filters.push(\`\${key}=gte.\${value}\`);
239
258
  }
240
259
 
241
260
  if ('lt' in advancedOps && advancedOps.lt !== undefined) {
242
- filters.push(\`\${key}=lt.\${advancedOps.lt}\`);
261
+ const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
262
+ filters.push(\`\${key}=lt.\${value}\`);
243
263
  }
244
264
 
245
265
  if ('lte' in advancedOps && advancedOps.lte !== undefined) {
246
- filters.push(\`\${key}=lte.\${advancedOps.lte}\`);
266
+ const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
267
+ filters.push(\`\${key}=lte.\${value}\`);
247
268
  }
248
269
 
249
270
  if ('in' in advancedOps && advancedOps.in?.length) {
@@ -301,19 +322,18 @@ export function buildFilterString<T>(where?: T): string | undefined {
301
322
  }
302
323
 
303
324
  /**
304
- * Apply filter to the query builder
325
+ * Apply a single condition group to the query builder
305
326
  */
306
- export function applyFilter<T>(
327
+ function applyConditionGroup<T>(
307
328
  query: SupabaseQueryBuilder,
308
- where: T
329
+ conditions: T
309
330
  ): SupabaseQueryBuilder {
310
- if (!where) return query;
331
+ if (!conditions) return query;
311
332
 
312
333
  let filteredQuery = query;
313
334
 
314
- // Apply each filter condition
315
- for (const [key, value] of Object.entries(where)) {
316
- if (value !== undefined) {
335
+ for (const [key, value] of Object.entries(conditions)) {
336
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
317
337
  if (typeof value === 'object' && value !== null) {
318
338
  // Handle advanced operators
319
339
  const advancedOps = value as unknown as FilterOperators<any>;
@@ -329,23 +349,31 @@ export function applyFilter<T>(
329
349
  }
330
350
 
331
351
  if ('gt' in advancedOps && advancedOps.gt !== undefined) {
352
+ // Convert Date objects to ISO strings for Supabase
353
+ const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
332
354
  // @ts-ignore: Supabase typing issue
333
- filteredQuery = filteredQuery.gt(key, advancedOps.gt);
355
+ filteredQuery = filteredQuery.gt(key, value);
334
356
  }
335
357
 
336
358
  if ('gte' in advancedOps && advancedOps.gte !== undefined) {
359
+ // Convert Date objects to ISO strings for Supabase
360
+ const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
337
361
  // @ts-ignore: Supabase typing issue
338
- filteredQuery = filteredQuery.gte(key, advancedOps.gte);
362
+ filteredQuery = filteredQuery.gte(key, value);
339
363
  }
340
364
 
341
365
  if ('lt' in advancedOps && advancedOps.lt !== undefined) {
366
+ // Convert Date objects to ISO strings for Supabase
367
+ const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
342
368
  // @ts-ignore: Supabase typing issue
343
- filteredQuery = filteredQuery.lt(key, advancedOps.lt);
369
+ filteredQuery = filteredQuery.lt(key, value);
344
370
  }
345
371
 
346
372
  if ('lte' in advancedOps && advancedOps.lte !== undefined) {
373
+ // Convert Date objects to ISO strings for Supabase
374
+ const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
347
375
  // @ts-ignore: Supabase typing issue
348
- filteredQuery = filteredQuery.lte(key, advancedOps.lte);
376
+ filteredQuery = filteredQuery.lte(key, value);
349
377
  }
350
378
 
351
379
  if ('in' in advancedOps && advancedOps.in?.length) {
@@ -409,6 +437,233 @@ export function applyFilter<T>(
409
437
  return filteredQuery;
410
438
  }
411
439
 
440
+ /**
441
+ * Apply filter to the query builder with OR/AND support
442
+ */
443
+ export function applyFilter<T>(
444
+ query: SupabaseQueryBuilder,
445
+ where: T
446
+ ): SupabaseQueryBuilder {
447
+ if (!where) return query;
448
+
449
+ const whereObj = where as any;
450
+ let filteredQuery = query;
451
+
452
+ // Handle regular conditions first (these are implicitly AND-ed)
453
+ filteredQuery = applyConditionGroup(filteredQuery, whereObj);
454
+
455
+ // Handle OR conditions
456
+ if (whereObj.OR && Array.isArray(whereObj.OR) && whereObj.OR.length > 0) {
457
+ // @ts-ignore: Supabase typing issue
458
+ filteredQuery = filteredQuery.or(
459
+ whereObj.OR.map((orCondition: any, index: number) => {
460
+ // Convert each OR condition to a filter string
461
+ const orFilters: string[] = [];
462
+
463
+ for (const [key, value] of Object.entries(orCondition)) {
464
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
465
+ if (typeof value === 'object' && value !== null) {
466
+ const advancedOps = value as unknown as FilterOperators<any>;
467
+
468
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
469
+ orFilters.push(\`\${key}.eq.\${advancedOps.equals}\`);
470
+ } else if ('not' in advancedOps && advancedOps.not !== undefined) {
471
+ orFilters.push(\`\${key}.neq.\${advancedOps.not}\`);
472
+ } else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
473
+ const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
474
+ orFilters.push(\`\${key}.gt.\${value}\`);
475
+ } else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
476
+ const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
477
+ orFilters.push(\`\${key}.gte.\${value}\`);
478
+ } else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
479
+ const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
480
+ orFilters.push(\`\${key}.lt.\${value}\`);
481
+ } else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
482
+ const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
483
+ orFilters.push(\`\${key}.lte.\${value}\`);
484
+ } else if ('in' in advancedOps && advancedOps.in?.length) {
485
+ orFilters.push(\`\${key}.in.(\${advancedOps.in.join(',')})\`);
486
+ } else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
487
+ orFilters.push(\`\${key}.ilike.*\${advancedOps.contains}*\`);
488
+ } else if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
489
+ orFilters.push(\`\${key}.ilike.\${advancedOps.startsWith}%\`);
490
+ } else if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
491
+ orFilters.push(\`\${key}.ilike.%\${advancedOps.endsWith}\`);
492
+ } else if ('has' in advancedOps && advancedOps.has !== undefined) {
493
+ orFilters.push(\`\${key}.ov.\${JSON.stringify(advancedOps.has)}\`);
494
+ } else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
495
+ orFilters.push(\`\${key}.cs.\${JSON.stringify(advancedOps.hasEvery)}\`);
496
+ } else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
497
+ orFilters.push(\`\${key}.ov.\${JSON.stringify(advancedOps.hasSome)}\`);
498
+ } else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
499
+ if (advancedOps.isEmpty) {
500
+ orFilters.push(\`\${key}.eq.{}\`);
501
+ } else {
502
+ orFilters.push(\`\${key}.neq.{}\`);
503
+ }
504
+ }
505
+ } else {
506
+ // Simple equality
507
+ orFilters.push(\`\${key}.eq.\${value}\`);
508
+ }
509
+ }
510
+ }
511
+
512
+ return orFilters.join(',');
513
+ }).join(',')
514
+ );
515
+ }
516
+
517
+ // Handle AND conditions (these are applied in addition to regular conditions)
518
+ if (whereObj.AND && Array.isArray(whereObj.AND) && whereObj.AND.length > 0) {
519
+ for (const andCondition of whereObj.AND) {
520
+ filteredQuery = applyConditionGroup(filteredQuery, andCondition);
521
+ }
522
+ }
523
+
524
+ return filteredQuery;
525
+ }
526
+
527
+ /**
528
+ * Evaluate if a record matches filter criteria (including OR/AND logic)
529
+ */
530
+ function matchesFilter<T>(record: any, filter: T): boolean {
531
+ if (!filter) return true;
532
+
533
+ const filterObj = filter as any;
534
+
535
+ // Separate regular conditions from OR/AND
536
+ const hasOr = filterObj.OR && Array.isArray(filterObj.OR) && filterObj.OR.length > 0;
537
+ const hasAnd = filterObj.AND && Array.isArray(filterObj.AND) && filterObj.AND.length > 0;
538
+
539
+ // Check regular field conditions (these are implicitly AND-ed)
540
+ const regularConditions: any = {};
541
+ for (const [key, value] of Object.entries(filterObj)) {
542
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
543
+ regularConditions[key] = value;
544
+ }
545
+ }
546
+
547
+ // Helper function to convert values to comparable format for date/time comparisons
548
+ const getComparableValue = (value: any): any => {
549
+ if (value instanceof Date) {
550
+ return value.getTime();
551
+ }
552
+ if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
553
+ // ISO date string
554
+ return new Date(value).getTime();
555
+ }
556
+ return value;
557
+ };
558
+
559
+ // Helper function to check individual field conditions
560
+ const checkFieldConditions = (conditions: any): boolean => {
561
+ for (const [key, value] of Object.entries(conditions)) {
562
+ if (value !== undefined) {
563
+ const recordValue = record[key];
564
+
565
+ if (typeof value === 'object' && value !== null) {
566
+ // Handle advanced operators
567
+ const advancedOps = value as unknown as FilterOperators<any>;
568
+
569
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
570
+ if (recordValue !== advancedOps.equals) return false;
571
+ }
572
+
573
+ if ('not' in advancedOps && advancedOps.not !== undefined) {
574
+ if (recordValue === advancedOps.not) return false;
575
+ }
576
+
577
+ if ('gt' in advancedOps && advancedOps.gt !== undefined) {
578
+ const recordComparable = getComparableValue(recordValue);
579
+ const filterComparable = getComparableValue(advancedOps.gt);
580
+ if (!(recordComparable > filterComparable)) return false;
581
+ }
582
+
583
+ if ('gte' in advancedOps && advancedOps.gte !== undefined) {
584
+ const recordComparable = getComparableValue(recordValue);
585
+ const filterComparable = getComparableValue(advancedOps.gte);
586
+ if (!(recordComparable >= filterComparable)) return false;
587
+ }
588
+
589
+ if ('lt' in advancedOps && advancedOps.lt !== undefined) {
590
+ const recordComparable = getComparableValue(recordValue);
591
+ const filterComparable = getComparableValue(advancedOps.lt);
592
+ if (!(recordComparable < filterComparable)) return false;
593
+ }
594
+
595
+ if ('lte' in advancedOps && advancedOps.lte !== undefined) {
596
+ const recordComparable = getComparableValue(recordValue);
597
+ const filterComparable = getComparableValue(advancedOps.lte);
598
+ if (!(recordComparable <= filterComparable)) return false;
599
+ }
600
+
601
+ if ('in' in advancedOps && advancedOps.in?.length) {
602
+ if (!advancedOps.in.includes(recordValue)) return false;
603
+ }
604
+
605
+ if ('contains' in advancedOps && advancedOps.contains !== undefined) {
606
+ if (!recordValue || !String(recordValue).toLowerCase().includes(String(advancedOps.contains).toLowerCase())) return false;
607
+ }
608
+
609
+ if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
610
+ if (!recordValue || !String(recordValue).toLowerCase().startsWith(String(advancedOps.startsWith).toLowerCase())) return false;
611
+ }
612
+
613
+ if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
614
+ if (!recordValue || !String(recordValue).toLowerCase().endsWith(String(advancedOps.endsWith).toLowerCase())) return false;
615
+ }
616
+
617
+ // Array-specific operators
618
+ if ('has' in advancedOps && advancedOps.has !== undefined) {
619
+ if (!Array.isArray(recordValue) || !advancedOps.has.some((item: any) => recordValue.includes(item))) return false;
620
+ }
621
+
622
+ if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
623
+ if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item: any) => recordValue.includes(item))) return false;
624
+ }
625
+
626
+ if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
627
+ if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item: any) => recordValue.includes(item))) return false;
628
+ }
629
+
630
+ if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
631
+ const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
632
+ if (isEmpty !== advancedOps.isEmpty) return false;
633
+ }
634
+ } else {
635
+ // Simple equality
636
+ if (recordValue !== value) return false;
637
+ }
638
+ }
639
+ }
640
+ return true;
641
+ };
642
+
643
+ // All conditions that must be true
644
+ const conditions: boolean[] = [];
645
+
646
+ // Regular field conditions (implicitly AND-ed)
647
+ if (Object.keys(regularConditions).length > 0) {
648
+ conditions.push(checkFieldConditions(regularConditions));
649
+ }
650
+
651
+ // AND conditions (all must be true)
652
+ if (hasAnd) {
653
+ const andResult = filterObj.AND.every((andCondition: any) => matchesFilter(record, andCondition));
654
+ conditions.push(andResult);
655
+ }
656
+
657
+ // OR conditions (at least one must be true)
658
+ if (hasOr) {
659
+ const orResult = filterObj.OR.some((orCondition: any) => matchesFilter(record, orCondition));
660
+ conditions.push(orResult);
661
+ }
662
+
663
+ // All conditions must be true
664
+ return conditions.every(condition => condition);
665
+ }
666
+
412
667
  /**
413
668
  * Apply order by to the query builder
414
669
  */
@@ -900,7 +1155,7 @@ export function createSuparismaHook<
900
1155
  }
901
1156
  }, []);
902
1157
 
903
- // Set up realtime subscription for the list
1158
+ // Set up realtime subscription for the list - ONCE and listen to ALL events
904
1159
  useEffect(() => {
905
1160
  if (!realtime) return;
906
1161
 
@@ -912,44 +1167,14 @@ export function createSuparismaHook<
912
1167
 
913
1168
  const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
914
1169
 
915
- // Check if we have complex array filters that should be handled client-side only
916
- let hasComplexArrayFilters = false;
917
- 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;
925
- }
926
- }
927
- }
928
- }
929
-
930
- // For complex array filters, use no database filter and rely on client-side filtering
931
- // For simple filters, use database-level filtering
1170
+ // ALWAYS listen to ALL events and filter client-side for maximum reliability
932
1171
  let subscriptionConfig: any = {
933
1172
  event: '*',
934
1173
  schema: 'public',
935
1174
  table: tableName,
936
1175
  };
937
1176
 
938
- if (hasComplexArrayFilters) {
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\`);
941
- } else if (where) {
942
- // Include filter for simple operations
943
- const filter = buildFilterString(where);
944
- if (filter) {
945
- subscriptionConfig.filter = filter;
946
- }
947
- console.log(\`Setting up subscription for \${tableName} with database filter: \${filter}\`);
948
- } else if (realtimeFilter) {
949
- // Use custom realtime filter if provided
950
- subscriptionConfig.filter = realtimeFilter;
951
- console.log(\`Setting up subscription for \${tableName} with custom filter: \${realtimeFilter}\`);
952
- }
1177
+ console.log(\`Setting up subscription for \${tableName} - listening to ALL events (client-side filtering)\`);
953
1178
 
954
1179
  const channel = supabase
955
1180
  .channel(channelId)
@@ -979,56 +1204,10 @@ export function createSuparismaHook<
979
1204
  console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
980
1205
 
981
1206
  // ALWAYS check if this record matches our filter client-side
982
- // This is especially important for complex array filters
983
- 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
- }
1027
-
1028
- if (!matchesFilter) {
1207
+ // This is especially important for complex OR/AND/array filters
1208
+ if (currentWhere && !matchesFilter(newRecord, currentWhere)) {
1029
1209
  console.log('New record does not match filter criteria, skipping');
1030
1210
  return prev;
1031
- }
1032
1211
  }
1033
1212
 
1034
1213
  // Check if record already exists (avoid duplicates)
@@ -1108,56 +1287,12 @@ export function createSuparismaHook<
1108
1287
  const updatedRecord = payload.new as TWithRelations;
1109
1288
 
1110
1289
  // Check if the updated record still matches our current filter
1111
- 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
- }
1152
-
1153
- // If the updated record doesn't match the filter, remove it from the list
1154
- if (!matchesFilter) {
1290
+ if (currentWhere && !matchesFilter(updatedRecord, currentWhere)) {
1155
1291
  console.log('Updated record no longer matches filter, removing from list');
1156
1292
  return prev.filter((item) =>
1157
1293
  // @ts-ignore: Supabase typing issue
1158
1294
  !('id' in item && 'id' in updatedRecord && item.id === updatedRecord.id)
1159
1295
  );
1160
- }
1161
1296
  }
1162
1297
 
1163
1298
  const newData = prev.map((item) =>
@@ -1324,7 +1459,7 @@ export function createSuparismaHook<
1324
1459
  searchTimeoutRef.current = null;
1325
1460
  }
1326
1461
  };
1327
- }, [realtime, channelName, tableName, where, initialLoadRef]); // Added 'where' back so subscription updates when filter changes
1462
+ }, [realtime, channelName, tableName]); // NEVER include 'where' - subscription should persist
1328
1463
 
1329
1464
  // Create a memoized options object to prevent unnecessary re-renders
1330
1465
  const optionsRef = useRef({ where, orderBy, limit, offset });
@@ -1353,7 +1488,7 @@ export function createSuparismaHook<
1353
1488
  return false;
1354
1489
  }, [where, orderBy, limit, offset]);
1355
1490
 
1356
- // Load initial data based on options
1491
+ // Load initial data and refetch when options change (BUT NEVER TOUCH SUBSCRIPTION)
1357
1492
  useEffect(() => {
1358
1493
  // Skip if search is active
1359
1494
  if (isSearchingRef.current) return;
@@ -1362,7 +1497,7 @@ export function createSuparismaHook<
1362
1497
  if (initialLoadRef.current) {
1363
1498
  // Only reload if options have changed significantly
1364
1499
  if (optionsChanged()) {
1365
- console.log(\`Options changed for \${tableName}, reloading data\`);
1500
+ console.log(\`Options changed for \${tableName}, refetching data (subscription stays alive)\`);
1366
1501
  findMany({
1367
1502
  where,
1368
1503
  orderBy,
@@ -1419,7 +1554,20 @@ export function createSuparismaHook<
1419
1554
  setLoading(true);
1420
1555
  setError(null);
1421
1556
 
1422
- const now = new Date().toISOString();
1557
+ const now = new Date();
1558
+
1559
+ // Helper function to convert Date objects to ISO strings for database
1560
+ const convertDatesForDatabase = (obj: any): any => {
1561
+ const result: any = {};
1562
+ for (const [key, value] of Object.entries(obj)) {
1563
+ if (value instanceof Date) {
1564
+ result[key] = value.toISOString();
1565
+ } else {
1566
+ result[key] = value;
1567
+ }
1568
+ }
1569
+ return result;
1570
+ };
1423
1571
 
1424
1572
  // Apply default values from schema
1425
1573
  const appliedDefaults: Record<string, any> = {};
@@ -1430,7 +1578,7 @@ export function createSuparismaHook<
1430
1578
  if (!(field in data)) {
1431
1579
  // Parse the default value based on its type
1432
1580
  if (defaultValue.includes('now()') || defaultValue.includes('now')) {
1433
- appliedDefaults[field] = now;
1581
+ appliedDefaults[field] = now.toISOString(); // Database expects ISO string
1434
1582
  } else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
1435
1583
  appliedDefaults[field] = crypto.randomUUID();
1436
1584
  } else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
@@ -1451,13 +1599,13 @@ export function createSuparismaHook<
1451
1599
  }
1452
1600
  }
1453
1601
 
1454
- const itemWithDefaults = {
1602
+ const itemWithDefaults = convertDatesForDatabase({
1455
1603
  ...appliedDefaults, // Apply schema defaults first
1456
1604
  ...data, // Then user data (overrides defaults)
1457
- // Use the actual field names from Prisma
1458
- ...(hasCreatedAt ? { [createdAtField]: now } : {}),
1459
- ...(hasUpdatedAt ? { [updatedAtField]: now } : {})
1460
- };
1605
+ // Use the actual field names from Prisma - convert Date to ISO string for database
1606
+ ...(hasCreatedAt ? { [createdAtField]: now.toISOString() } : {}),
1607
+ ...(hasUpdatedAt ? { [updatedAtField]: now.toISOString() } : {})
1608
+ });
1461
1609
 
1462
1610
  const { data: result, error } = await supabase
1463
1611
  .from(tableName)
@@ -1525,17 +1673,30 @@ export function createSuparismaHook<
1525
1673
  throw new Error('A unique identifier is required');
1526
1674
  }
1527
1675
 
1528
- const now = new Date().toISOString();
1676
+ const now = new Date();
1677
+
1678
+ // Helper function to convert Date objects to ISO strings for database
1679
+ const convertDatesForDatabase = (obj: any): any => {
1680
+ const result: any = {};
1681
+ for (const [key, value] of Object.entries(obj)) {
1682
+ if (value instanceof Date) {
1683
+ result[key] = value.toISOString();
1684
+ } else {
1685
+ result[key] = value;
1686
+ }
1687
+ }
1688
+ return result;
1689
+ };
1529
1690
 
1530
1691
  // We do not apply default values for updates
1531
1692
  // Default values are only for creation, not for updates,
1532
1693
  // as existing records already have these values set
1533
1694
 
1534
- const itemWithDefaults = {
1695
+ const itemWithDefaults = convertDatesForDatabase({
1535
1696
  ...params.data,
1536
- // Use the actual updatedAt field name from Prisma
1537
- ...(hasUpdatedAt ? { [updatedAtField]: now } : {})
1538
- };
1697
+ // Use the actual updatedAt field name from Prisma - convert Date to ISO string for database
1698
+ ...(hasUpdatedAt ? { [updatedAtField]: now.toISOString() } : {})
1699
+ });
1539
1700
 
1540
1701
  const { data, error } = await supabase
1541
1702
  .from(tableName)
@@ -69,7 +69,7 @@ function generateModelTypesFile(model) {
69
69
  baseType = 'boolean';
70
70
  break;
71
71
  case 'DateTime':
72
- baseType = 'string'; // ISO date string
72
+ baseType = 'Date'; // Proper Date type for DateTime fields
73
73
  break;
74
74
  case 'Json':
75
75
  baseType = 'any'; // Or a more specific structured type if available
@@ -219,6 +219,7 @@ export type ${modelName}UpdateInput = Partial<${modelName}CreateInput>;
219
219
  /**
220
220
  * Filter type for querying ${modelName} records.
221
221
  * You can filter by any field in the model using equality or advanced filter operators.
222
+ * Supports OR and AND logical operations for complex queries.
222
223
  *
223
224
  * @example
224
225
  * // Basic filtering
@@ -250,6 +251,40 @@ ${withRelationsProps
250
251
  * });
251
252
  *
252
253
  * @example
254
+ * // OR conditions - match ANY condition
255
+ * ${modelName.toLowerCase()}.findMany({
256
+ * where: {
257
+ * OR: [
258
+ * { ${withRelationsProps.slice(0, 1).map(p => p.trim().split(':')[0].trim())[0] || 'field1'}: "value1" },
259
+ * { ${withRelationsProps.slice(1, 2).map(p => p.trim().split(':')[0].trim())[0] || 'field2'}: { contains: "value2" } }
260
+ * ]
261
+ * }
262
+ * });
263
+ *
264
+ * @example
265
+ * // AND conditions - match ALL conditions
266
+ * ${modelName.toLowerCase()}.findMany({
267
+ * where: {
268
+ * AND: [
269
+ * { ${withRelationsProps.slice(0, 1).map(p => p.trim().split(':')[0].trim())[0] || 'field1'}: "value1" },
270
+ * { ${withRelationsProps.slice(1, 2).map(p => p.trim().split(':')[0].trim())[0] || 'field2'}: { gt: 100 } }
271
+ * ]
272
+ * }
273
+ * });
274
+ *
275
+ * @example
276
+ * // Complex nested logic
277
+ * ${modelName.toLowerCase()}.findMany({
278
+ * where: {
279
+ * active: true, // Regular condition (implicit AND)
280
+ * OR: [
281
+ * { role: "admin" },
282
+ * { role: "moderator" }
283
+ * ]
284
+ * }
285
+ * });
286
+ *
287
+ * @example
253
288
  * // Array filtering (for array fields)
254
289
  * ${modelName.toLowerCase()}.findMany({
255
290
  * where: {
@@ -286,6 +321,11 @@ ${model.fields
286
321
  return '';
287
322
  }).filter(Boolean))
288
323
  .join('\n')}
324
+ } & {
325
+ /** Match ANY of the provided conditions */
326
+ OR?: ${modelName}WhereInput[];
327
+ /** Match ALL of the provided conditions */
328
+ AND?: ${modelName}WhereInput[];
289
329
  };
290
330
 
291
331
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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": {