suparisma 1.0.12 → 1.1.1

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>;
@@ -301,19 +318,18 @@ export function buildFilterString<T>(where?: T): string | undefined {
301
318
  }
302
319
 
303
320
  /**
304
- * Apply filter to the query builder
321
+ * Apply a single condition group to the query builder
305
322
  */
306
- export function applyFilter<T>(
323
+ function applyConditionGroup<T>(
307
324
  query: SupabaseQueryBuilder,
308
- where: T
325
+ conditions: T
309
326
  ): SupabaseQueryBuilder {
310
- if (!where) return query;
327
+ if (!conditions) return query;
311
328
 
312
329
  let filteredQuery = query;
313
330
 
314
- // Apply each filter condition
315
- for (const [key, value] of Object.entries(where)) {
316
- if (value !== undefined) {
331
+ for (const [key, value] of Object.entries(conditions)) {
332
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
317
333
  if (typeof value === 'object' && value !== null) {
318
334
  // Handle advanced operators
319
335
  const advancedOps = value as unknown as FilterOperators<any>;
@@ -409,6 +425,209 @@ export function applyFilter<T>(
409
425
  return filteredQuery;
410
426
  }
411
427
 
428
+ /**
429
+ * Apply filter to the query builder with OR/AND support
430
+ */
431
+ export function applyFilter<T>(
432
+ query: SupabaseQueryBuilder,
433
+ where: T
434
+ ): SupabaseQueryBuilder {
435
+ if (!where) return query;
436
+
437
+ const whereObj = where as any;
438
+ let filteredQuery = query;
439
+
440
+ // Handle regular conditions first (these are implicitly AND-ed)
441
+ filteredQuery = applyConditionGroup(filteredQuery, whereObj);
442
+
443
+ // Handle OR conditions
444
+ if (whereObj.OR && Array.isArray(whereObj.OR) && whereObj.OR.length > 0) {
445
+ // @ts-ignore: Supabase typing issue
446
+ filteredQuery = filteredQuery.or(
447
+ whereObj.OR.map((orCondition: any, index: number) => {
448
+ // Convert each OR condition to a filter string
449
+ const orFilters: string[] = [];
450
+
451
+ for (const [key, value] of Object.entries(orCondition)) {
452
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
453
+ if (typeof value === 'object' && value !== null) {
454
+ const advancedOps = value as unknown as FilterOperators<any>;
455
+
456
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
457
+ orFilters.push(\`\${key}.eq.\${advancedOps.equals}\`);
458
+ } else if ('not' in advancedOps && advancedOps.not !== undefined) {
459
+ orFilters.push(\`\${key}.neq.\${advancedOps.not}\`);
460
+ } else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
461
+ orFilters.push(\`\${key}.gt.\${advancedOps.gt}\`);
462
+ } else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
463
+ orFilters.push(\`\${key}.gte.\${advancedOps.gte}\`);
464
+ } else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
465
+ orFilters.push(\`\${key}.lt.\${advancedOps.lt}\`);
466
+ } else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
467
+ orFilters.push(\`\${key}.lte.\${advancedOps.lte}\`);
468
+ } else if ('in' in advancedOps && advancedOps.in?.length) {
469
+ orFilters.push(\`\${key}.in.(\${advancedOps.in.join(',')})\`);
470
+ } else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
471
+ orFilters.push(\`\${key}.ilike.*\${advancedOps.contains}*\`);
472
+ } else if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
473
+ orFilters.push(\`\${key}.ilike.\${advancedOps.startsWith}%\`);
474
+ } else if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
475
+ orFilters.push(\`\${key}.ilike.%\${advancedOps.endsWith}\`);
476
+ } else if ('has' in advancedOps && advancedOps.has !== undefined) {
477
+ orFilters.push(\`\${key}.ov.\${JSON.stringify(advancedOps.has)}\`);
478
+ } else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
479
+ orFilters.push(\`\${key}.cs.\${JSON.stringify(advancedOps.hasEvery)}\`);
480
+ } else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
481
+ orFilters.push(\`\${key}.ov.\${JSON.stringify(advancedOps.hasSome)}\`);
482
+ } else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
483
+ if (advancedOps.isEmpty) {
484
+ orFilters.push(\`\${key}.eq.{}\`);
485
+ } else {
486
+ orFilters.push(\`\${key}.neq.{}\`);
487
+ }
488
+ }
489
+ } else {
490
+ // Simple equality
491
+ orFilters.push(\`\${key}.eq.\${value}\`);
492
+ }
493
+ }
494
+ }
495
+
496
+ return orFilters.join(',');
497
+ }).join(',')
498
+ );
499
+ }
500
+
501
+ // Handle AND conditions (these are applied in addition to regular conditions)
502
+ if (whereObj.AND && Array.isArray(whereObj.AND) && whereObj.AND.length > 0) {
503
+ for (const andCondition of whereObj.AND) {
504
+ filteredQuery = applyConditionGroup(filteredQuery, andCondition);
505
+ }
506
+ }
507
+
508
+ return filteredQuery;
509
+ }
510
+
511
+ /**
512
+ * Evaluate if a record matches filter criteria (including OR/AND logic)
513
+ */
514
+ function matchesFilter<T>(record: any, filter: T): boolean {
515
+ if (!filter) return true;
516
+
517
+ const filterObj = filter as any;
518
+
519
+ // Separate regular conditions from OR/AND
520
+ const hasOr = filterObj.OR && Array.isArray(filterObj.OR) && filterObj.OR.length > 0;
521
+ const hasAnd = filterObj.AND && Array.isArray(filterObj.AND) && filterObj.AND.length > 0;
522
+
523
+ // Check regular field conditions (these are implicitly AND-ed)
524
+ const regularConditions: any = {};
525
+ for (const [key, value] of Object.entries(filterObj)) {
526
+ if (value !== undefined && key !== 'OR' && key !== 'AND') {
527
+ regularConditions[key] = value;
528
+ }
529
+ }
530
+
531
+ // Helper function to check individual field conditions
532
+ const checkFieldConditions = (conditions: any): boolean => {
533
+ for (const [key, value] of Object.entries(conditions)) {
534
+ if (value !== undefined) {
535
+ const recordValue = record[key];
536
+
537
+ if (typeof value === 'object' && value !== null) {
538
+ // Handle advanced operators
539
+ const advancedOps = value as unknown as FilterOperators<any>;
540
+
541
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
542
+ if (recordValue !== advancedOps.equals) return false;
543
+ }
544
+
545
+ if ('not' in advancedOps && advancedOps.not !== undefined) {
546
+ if (recordValue === advancedOps.not) return false;
547
+ }
548
+
549
+ if ('gt' in advancedOps && advancedOps.gt !== undefined) {
550
+ if (!(recordValue > advancedOps.gt)) return false;
551
+ }
552
+
553
+ if ('gte' in advancedOps && advancedOps.gte !== undefined) {
554
+ if (!(recordValue >= advancedOps.gte)) return false;
555
+ }
556
+
557
+ if ('lt' in advancedOps && advancedOps.lt !== undefined) {
558
+ if (!(recordValue < advancedOps.lt)) return false;
559
+ }
560
+
561
+ if ('lte' in advancedOps && advancedOps.lte !== undefined) {
562
+ if (!(recordValue <= advancedOps.lte)) return false;
563
+ }
564
+
565
+ if ('in' in advancedOps && advancedOps.in?.length) {
566
+ if (!advancedOps.in.includes(recordValue)) return false;
567
+ }
568
+
569
+ if ('contains' in advancedOps && advancedOps.contains !== undefined) {
570
+ if (!recordValue || !String(recordValue).toLowerCase().includes(String(advancedOps.contains).toLowerCase())) return false;
571
+ }
572
+
573
+ if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
574
+ if (!recordValue || !String(recordValue).toLowerCase().startsWith(String(advancedOps.startsWith).toLowerCase())) return false;
575
+ }
576
+
577
+ if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
578
+ if (!recordValue || !String(recordValue).toLowerCase().endsWith(String(advancedOps.endsWith).toLowerCase())) return false;
579
+ }
580
+
581
+ // Array-specific operators
582
+ if ('has' in advancedOps && advancedOps.has !== undefined) {
583
+ if (!Array.isArray(recordValue) || !advancedOps.has.some((item: any) => recordValue.includes(item))) return false;
584
+ }
585
+
586
+ if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
587
+ if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item: any) => recordValue.includes(item))) return false;
588
+ }
589
+
590
+ if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
591
+ if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item: any) => recordValue.includes(item))) return false;
592
+ }
593
+
594
+ if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
595
+ const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
596
+ if (isEmpty !== advancedOps.isEmpty) return false;
597
+ }
598
+ } else {
599
+ // Simple equality
600
+ if (recordValue !== value) return false;
601
+ }
602
+ }
603
+ }
604
+ return true;
605
+ };
606
+
607
+ // All conditions that must be true
608
+ const conditions: boolean[] = [];
609
+
610
+ // Regular field conditions (implicitly AND-ed)
611
+ if (Object.keys(regularConditions).length > 0) {
612
+ conditions.push(checkFieldConditions(regularConditions));
613
+ }
614
+
615
+ // AND conditions (all must be true)
616
+ if (hasAnd) {
617
+ const andResult = filterObj.AND.every((andCondition: any) => matchesFilter(record, andCondition));
618
+ conditions.push(andResult);
619
+ }
620
+
621
+ // OR conditions (at least one must be true)
622
+ if (hasOr) {
623
+ const orResult = filterObj.OR.some((orCondition: any) => matchesFilter(record, orCondition));
624
+ conditions.push(orResult);
625
+ }
626
+
627
+ // All conditions must be true
628
+ return conditions.every(condition => condition);
629
+ }
630
+
412
631
  /**
413
632
  * Apply order by to the query builder
414
633
  */
@@ -900,7 +1119,7 @@ export function createSuparismaHook<
900
1119
  }
901
1120
  }, []);
902
1121
 
903
- // Set up realtime subscription for the list
1122
+ // Set up realtime subscription for the list - ONCE and listen to ALL events
904
1123
  useEffect(() => {
905
1124
  if (!realtime) return;
906
1125
 
@@ -912,44 +1131,14 @@ export function createSuparismaHook<
912
1131
 
913
1132
  const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
914
1133
 
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
1134
+ // ALWAYS listen to ALL events and filter client-side for maximum reliability
932
1135
  let subscriptionConfig: any = {
933
1136
  event: '*',
934
1137
  schema: 'public',
935
1138
  table: tableName,
936
1139
  };
937
1140
 
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
- }
1141
+ console.log(\`Setting up subscription for \${tableName} - listening to ALL events (client-side filtering)\`);
953
1142
 
954
1143
  const channel = supabase
955
1144
  .channel(channelId)
@@ -979,56 +1168,10 @@ export function createSuparismaHook<
979
1168
  console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
980
1169
 
981
1170
  // 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) {
1171
+ // This is especially important for complex OR/AND/array filters
1172
+ if (currentWhere && !matchesFilter(newRecord, currentWhere)) {
1029
1173
  console.log('New record does not match filter criteria, skipping');
1030
1174
  return prev;
1031
- }
1032
1175
  }
1033
1176
 
1034
1177
  // Check if record already exists (avoid duplicates)
@@ -1108,56 +1251,12 @@ export function createSuparismaHook<
1108
1251
  const updatedRecord = payload.new as TWithRelations;
1109
1252
 
1110
1253
  // 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) {
1254
+ if (currentWhere && !matchesFilter(updatedRecord, currentWhere)) {
1155
1255
  console.log('Updated record no longer matches filter, removing from list');
1156
1256
  return prev.filter((item) =>
1157
1257
  // @ts-ignore: Supabase typing issue
1158
1258
  !('id' in item && 'id' in updatedRecord && item.id === updatedRecord.id)
1159
1259
  );
1160
- }
1161
1260
  }
1162
1261
 
1163
1262
  const newData = prev.map((item) =>
@@ -1324,7 +1423,7 @@ export function createSuparismaHook<
1324
1423
  searchTimeoutRef.current = null;
1325
1424
  }
1326
1425
  };
1327
- }, [realtime, channelName, tableName, where, initialLoadRef]); // Added 'where' back so subscription updates when filter changes
1426
+ }, [realtime, channelName, tableName]); // NEVER include 'where' - subscription should persist
1328
1427
 
1329
1428
  // Create a memoized options object to prevent unnecessary re-renders
1330
1429
  const optionsRef = useRef({ where, orderBy, limit, offset });
@@ -1353,7 +1452,7 @@ export function createSuparismaHook<
1353
1452
  return false;
1354
1453
  }, [where, orderBy, limit, offset]);
1355
1454
 
1356
- // Load initial data based on options
1455
+ // Load initial data and refetch when options change (BUT NEVER TOUCH SUBSCRIPTION)
1357
1456
  useEffect(() => {
1358
1457
  // Skip if search is active
1359
1458
  if (isSearchingRef.current) return;
@@ -1362,7 +1461,7 @@ export function createSuparismaHook<
1362
1461
  if (initialLoadRef.current) {
1363
1462
  // Only reload if options have changed significantly
1364
1463
  if (optionsChanged()) {
1365
- console.log(\`Options changed for \${tableName}, reloading data\`);
1464
+ console.log(\`Options changed for \${tableName}, refetching data (subscription stays alive)\`);
1366
1465
  findMany({
1367
1466
  where,
1368
1467
  orderBy,
@@ -1392,6 +1491,8 @@ export function createSuparismaHook<
1392
1491
  /**
1393
1492
  * Create a new record with the provided data.
1394
1493
  * Default values from the schema will be applied if not provided.
1494
+ * NOTE: This operation does NOT immediately update the local state.
1495
+ * The state will be updated when the realtime INSERT event is received.
1395
1496
  *
1396
1497
  * @param data - The data to create the record with
1397
1498
  * @returns A promise with the created record or error
@@ -1464,8 +1565,8 @@ export function createSuparismaHook<
1464
1565
 
1465
1566
  if (error) throw error;
1466
1567
 
1467
- // Update the total count after a successful creation
1468
- setTimeout(() => fetchTotalCount(), 0);
1568
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime INSERT event handle it
1569
+ console.log('✅ Created ' + tableName + ' record, waiting for realtime INSERT event to update state');
1469
1570
 
1470
1571
  // Return created record
1471
1572
  return { data: result?.[0] as TWithRelations, error: null };
@@ -1476,10 +1577,12 @@ export function createSuparismaHook<
1476
1577
  } finally {
1477
1578
  setLoading(false);
1478
1579
  }
1479
- }, [fetchTotalCount]);
1580
+ }, []);
1480
1581
 
1481
1582
  /**
1482
1583
  * Update an existing record identified by a unique identifier.
1584
+ * NOTE: This operation does NOT immediately update the local state.
1585
+ * The state will be updated when the realtime UPDATE event is received.
1483
1586
  *
1484
1587
  * @param params - Object containing the identifier and update data
1485
1588
  * @returns A promise with the updated record or error
@@ -1541,10 +1644,8 @@ export function createSuparismaHook<
1541
1644
 
1542
1645
  if (error) throw error;
1543
1646
 
1544
- // Update the total count after a successful update
1545
- // This is for consistency with other operations, and because
1546
- // updates can sometimes affect filtering based on updated values
1547
- setTimeout(() => fetchTotalCount(), 0);
1647
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime UPDATE event handle it
1648
+ console.log('✅ Updated ' + tableName + ' record, waiting for realtime UPDATE event to update state');
1548
1649
 
1549
1650
  // Return updated record
1550
1651
  return { data: data?.[0] as TWithRelations, error: null };
@@ -1555,10 +1656,12 @@ export function createSuparismaHook<
1555
1656
  } finally {
1556
1657
  setLoading(false);
1557
1658
  }
1558
- }, [fetchTotalCount]);
1659
+ }, []);
1559
1660
 
1560
1661
  /**
1561
1662
  * Delete a record by its unique identifier.
1663
+ * NOTE: This operation does NOT immediately update the local state.
1664
+ * The state will be updated when the realtime DELETE event is received.
1562
1665
  *
1563
1666
  * @param where - The unique identifier to delete the record by
1564
1667
  * @returns A promise with the deleted record or error
@@ -1608,8 +1711,8 @@ export function createSuparismaHook<
1608
1711
 
1609
1712
  if (error) throw error;
1610
1713
 
1611
- // Update the total count after a successful deletion
1612
- setTimeout(() => fetchTotalCount(), 0);
1714
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE event handle it
1715
+ console.log('✅ Deleted ' + tableName + ' record, waiting for realtime DELETE event to update state');
1613
1716
 
1614
1717
  // Return the deleted record
1615
1718
  return { data: recordToDelete as TWithRelations, error: null };
@@ -1620,10 +1723,12 @@ export function createSuparismaHook<
1620
1723
  } finally {
1621
1724
  setLoading(false);
1622
1725
  }
1623
- }, [fetchTotalCount]);
1726
+ }, []);
1624
1727
 
1625
1728
  /**
1626
1729
  * Delete multiple records matching the filter criteria.
1730
+ * NOTE: This operation does NOT immediately update the local state.
1731
+ * The state will be updated when realtime DELETE events are received for each record.
1627
1732
  *
1628
1733
  * @param params - Query parameters for filtering records to delete
1629
1734
  * @returns A promise with the count of deleted records or error
@@ -1633,7 +1738,7 @@ export function createSuparismaHook<
1633
1738
  * const result = await users.deleteMany({
1634
1739
  * where: { active: false }
1635
1740
  * });
1636
- * console.log(\`Deleted \${result.count} inactive users\`);
1741
+ * console.log('Deleted ' + result.count + ' inactive users');
1637
1742
  *
1638
1743
  * @example
1639
1744
  * // Delete all records (use with caution!)
@@ -1677,8 +1782,8 @@ export function createSuparismaHook<
1677
1782
 
1678
1783
  if (deleteError) throw deleteError;
1679
1784
 
1680
- // Update the total count after a successful bulk deletion
1681
- setTimeout(() => fetchTotalCount(), 0);
1785
+ // DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE events handle it
1786
+ console.log('✅ Deleted ' + recordsToDelete.length + ' ' + tableName + ' records, waiting for realtime DELETE events to update state');
1682
1787
 
1683
1788
  // Return the count of deleted records
1684
1789
  return { count: recordsToDelete.length, error: null };
@@ -1689,7 +1794,7 @@ export function createSuparismaHook<
1689
1794
  } finally {
1690
1795
  setLoading(false);
1691
1796
  }
1692
- }, [fetchTotalCount]);
1797
+ }, []);
1693
1798
 
1694
1799
  /**
1695
1800
  * Find the first record matching the filter criteria.
@@ -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.0.12",
3
+ "version": "1.1.1",
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": {