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.
- package/dist/generators/coreGenerator.js +320 -159
- package/dist/generators/typeGenerator.js +41 -1
- 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>;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
325
|
+
* Apply a single condition group to the query builder
|
|
305
326
|
*/
|
|
306
|
-
|
|
327
|
+
function applyConditionGroup<T>(
|
|
307
328
|
query: SupabaseQueryBuilder,
|
|
308
|
-
|
|
329
|
+
conditions: T
|
|
309
330
|
): SupabaseQueryBuilder {
|
|
310
|
-
if (!
|
|
331
|
+
if (!conditions) return query;
|
|
311
332
|
|
|
312
333
|
let filteredQuery = query;
|
|
313
334
|
|
|
314
|
-
|
|
315
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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},
|
|
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()
|
|
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()
|
|
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 = '
|
|
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