suparisma 1.1.0 → 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.
- package/dist/generators/coreGenerator.js +238 -139
- package/dist/generators/typeGenerator.js +40 -0
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
|
321
|
+
* Apply a single condition group to the query builder
|
|
305
322
|
*/
|
|
306
|
-
|
|
323
|
+
function applyConditionGroup<T>(
|
|
307
324
|
query: SupabaseQueryBuilder,
|
|
308
|
-
|
|
325
|
+
conditions: T
|
|
309
326
|
): SupabaseQueryBuilder {
|
|
310
|
-
if (!
|
|
327
|
+
if (!conditions) return query;
|
|
311
328
|
|
|
312
329
|
let filteredQuery = query;
|
|
313
330
|
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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},
|
|
1464
|
+
console.log(\`Options changed for \${tableName}, refetching data (subscription stays alive)\`);
|
|
1366
1465
|
findMany({
|
|
1367
1466
|
where,
|
|
1368
1467
|
orderBy,
|
|
@@ -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