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.
- package/dist/generators/coreGenerator.js +259 -154
- 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,
|
|
@@ -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
|
-
//
|
|
1468
|
-
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
1545
|
-
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
1612
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
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
|
-
//
|
|
1681
|
-
|
|
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
|
-
}, [
|
|
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