suparisma 1.2.3 → 1.2.7
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 +97 -37
- package/dist/generators/hookGenerator.js +6 -2
- package/dist/generators/typeGenerator.js +43 -31
- package/dist/index.js +6 -1
- package/dist/suparisma/generated/utils/core.js +1835 -0
- package/package.json +1 -1
- package/tmp/generated-test/hooks/useSuparismaAsset.ts +94 -0
- package/tmp/generated-test/hooks/useSuparismaChapter.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaCourse.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaDeviceSession.ts +94 -0
- package/tmp/generated-test/hooks/useSuparismaEnrollment.ts +92 -0
- package/tmp/generated-test/hooks/useSuparismaLesson.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaLessonPurchase.ts +92 -0
- package/tmp/generated-test/hooks/useSuparismaLessonQuestion.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaPayoutMethod.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaPayoutRequest.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaQuestionOption.ts +92 -0
- package/tmp/generated-test/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaTeacherPayoutInfo.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaThing.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaUser.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaVideoNote.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaWallet.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaWalletTransaction.ts +96 -0
- package/tmp/generated-test/hooks/useSuparismaWatchProgress.ts +96 -0
- package/tmp/generated-test/index.ts +140 -0
- package/tmp/generated-test/types/AssetTypes.ts +485 -0
- package/tmp/generated-test/types/ChapterTypes.ts +488 -0
- package/tmp/generated-test/types/CourseTypes.ts +519 -0
- package/tmp/generated-test/types/DeviceSessionTypes.ts +489 -0
- package/tmp/generated-test/types/EnrollmentTypes.ts +495 -0
- package/tmp/generated-test/types/LessonPurchaseTypes.ts +490 -0
- package/tmp/generated-test/types/LessonQuestionTypes.ts +496 -0
- package/tmp/generated-test/types/LessonTypes.ts +517 -0
- package/tmp/generated-test/types/PayoutMethodTypes.ts +517 -0
- package/tmp/generated-test/types/PayoutRequestTypes.ts +528 -0
- package/tmp/generated-test/types/QuestionOptionTypes.ts +479 -0
- package/tmp/generated-test/types/SavedPaymentMethodTypes.ts +497 -0
- package/tmp/generated-test/types/TeacherPayoutInfoTypes.ts +480 -0
- package/tmp/generated-test/types/ThingTypes.ts +482 -0
- package/tmp/generated-test/types/UserTypes.ts +487 -0
- package/tmp/generated-test/types/VideoNoteTypes.ts +489 -0
- package/tmp/generated-test/types/WalletTransactionTypes.ts +505 -0
- package/tmp/generated-test/types/WalletTypes.ts +480 -0
- package/tmp/generated-test/types/WatchProgressTypes.ts +493 -0
- package/tmp/generated-test/utils/core.ts +2306 -0
- package/tmp/generated-test/utils/supabase-client.ts +17 -0
- package/tmp/generated-test2/hooks/useSuparismaAsset.ts +94 -0
- package/tmp/generated-test2/hooks/useSuparismaChapter.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaCourse.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaDeviceSession.ts +94 -0
- package/tmp/generated-test2/hooks/useSuparismaEnrollment.ts +92 -0
- package/tmp/generated-test2/hooks/useSuparismaLesson.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaLessonPurchase.ts +92 -0
- package/tmp/generated-test2/hooks/useSuparismaLessonQuestion.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaPayoutMethod.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaPayoutRequest.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaQuestionOption.ts +92 -0
- package/tmp/generated-test2/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaTeacherPayoutInfo.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaThing.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaUser.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaVideoNote.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaWallet.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaWalletTransaction.ts +96 -0
- package/tmp/generated-test2/hooks/useSuparismaWatchProgress.ts +96 -0
- package/tmp/generated-test2/index.ts +140 -0
- package/tmp/generated-test2/types/AssetTypes.ts +485 -0
- package/tmp/generated-test2/types/ChapterTypes.ts +488 -0
- package/tmp/generated-test2/types/CourseTypes.ts +522 -0
- package/tmp/generated-test2/types/DeviceSessionTypes.ts +489 -0
- package/tmp/generated-test2/types/EnrollmentTypes.ts +495 -0
- package/tmp/generated-test2/types/LessonPurchaseTypes.ts +490 -0
- package/tmp/generated-test2/types/LessonQuestionTypes.ts +496 -0
- package/tmp/generated-test2/types/LessonTypes.ts +517 -0
- package/tmp/generated-test2/types/PayoutMethodTypes.ts +517 -0
- package/tmp/generated-test2/types/PayoutRequestTypes.ts +528 -0
- package/tmp/generated-test2/types/QuestionOptionTypes.ts +479 -0
- package/tmp/generated-test2/types/SavedPaymentMethodTypes.ts +497 -0
- package/tmp/generated-test2/types/TeacherPayoutInfoTypes.ts +480 -0
- package/tmp/generated-test2/types/ThingTypes.ts +482 -0
- package/tmp/generated-test2/types/UserTypes.ts +490 -0
- package/tmp/generated-test2/types/VideoNoteTypes.ts +489 -0
- package/tmp/generated-test2/types/WalletTransactionTypes.ts +505 -0
- package/tmp/generated-test2/types/WalletTypes.ts +480 -0
- package/tmp/generated-test2/types/WatchProgressTypes.ts +493 -0
- package/tmp/generated-test2/utils/core.ts +2306 -0
- package/tmp/generated-test2/utils/supabase-client.ts +17 -0
- package/tmp/generated-test3/hooks/useSuparismaAsset.ts +94 -0
- package/tmp/generated-test3/hooks/useSuparismaChapter.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaCourse.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaDeviceSession.ts +94 -0
- package/tmp/generated-test3/hooks/useSuparismaEnrollment.ts +94 -0
- package/tmp/generated-test3/hooks/useSuparismaLesson.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaLessonPurchase.ts +94 -0
- package/tmp/generated-test3/hooks/useSuparismaLessonQuestion.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaPayoutMethod.ts +96 -0
- package/tmp/generated-test3/hooks/useSuparismaPayoutRequest.ts +96 -0
- package/tmp/generated-test3/hooks/useSuparismaQuestionOption.ts +94 -0
- package/tmp/generated-test3/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
- package/tmp/generated-test3/hooks/useSuparismaTeacherPayoutInfo.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaThing.ts +96 -0
- package/tmp/generated-test3/hooks/useSuparismaUser.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaVideoNote.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaWallet.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaWalletTransaction.ts +98 -0
- package/tmp/generated-test3/hooks/useSuparismaWatchProgress.ts +98 -0
- package/tmp/generated-test3/index.ts +140 -0
- package/tmp/generated-test3/types/AssetTypes.ts +485 -0
- package/tmp/generated-test3/types/ChapterTypes.ts +488 -0
- package/tmp/generated-test3/types/CourseTypes.ts +522 -0
- package/tmp/generated-test3/types/DeviceSessionTypes.ts +489 -0
- package/tmp/generated-test3/types/EnrollmentTypes.ts +495 -0
- package/tmp/generated-test3/types/LessonPurchaseTypes.ts +490 -0
- package/tmp/generated-test3/types/LessonQuestionTypes.ts +496 -0
- package/tmp/generated-test3/types/LessonTypes.ts +517 -0
- package/tmp/generated-test3/types/PayoutMethodTypes.ts +517 -0
- package/tmp/generated-test3/types/PayoutRequestTypes.ts +528 -0
- package/tmp/generated-test3/types/QuestionOptionTypes.ts +479 -0
- package/tmp/generated-test3/types/SavedPaymentMethodTypes.ts +497 -0
- package/tmp/generated-test3/types/TeacherPayoutInfoTypes.ts +480 -0
- package/tmp/generated-test3/types/ThingTypes.ts +482 -0
- package/tmp/generated-test3/types/UserTypes.ts +490 -0
- package/tmp/generated-test3/types/VideoNoteTypes.ts +489 -0
- package/tmp/generated-test3/types/WalletTransactionTypes.ts +505 -0
- package/tmp/generated-test3/types/WalletTypes.ts +480 -0
- package/tmp/generated-test3/types/WatchProgressTypes.ts +493 -0
- package/tmp/generated-test3/utils/core.ts +2316 -0
- package/tmp/generated-test3/utils/supabase-client.ts +17 -0
- package/tmp/prisma-test-schema-2.prisma +339 -0
- package/tmp/prisma-test-schema.prisma +317 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
|
|
3
|
+
// Edit the generator script instead: scripts/generate-realtime-hooks.ts
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.escapeRegexCharacters = escapeRegexCharacters;
|
|
6
|
+
exports.generateUUID = generateUUID;
|
|
7
|
+
exports.buildFilterString = buildFilterString;
|
|
8
|
+
exports.applyFilter = applyFilter;
|
|
9
|
+
exports.buildSelectString = buildSelectString;
|
|
10
|
+
exports.applyOrderBy = applyOrderBy;
|
|
11
|
+
exports.createSuparismaHook = createSuparismaHook;
|
|
12
|
+
const react_1 = require("react");
|
|
13
|
+
// This import should be relative to its new location in utils/
|
|
14
|
+
const supabase_client_1 = require("./supabase-client");
|
|
15
|
+
/**
|
|
16
|
+
* Utility function to escape regex special characters for safe RegExp usage
|
|
17
|
+
* Prevents "Invalid regular expression" errors when search terms contain special characters
|
|
18
|
+
*/
|
|
19
|
+
function escapeRegexCharacters(str) {
|
|
20
|
+
// Escape all special regex characters: ( ) [ ] { } + * ? ^ $ | . \
|
|
21
|
+
return str.replace(/[()\[\]{}+*?^$|.\\]/g, '\\\\$&');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a UUID v4, with fallback for environments without crypto.randomUUID()
|
|
25
|
+
* Works in: browsers, Node.js, and React Native (with react-native-get-random-values polyfill)
|
|
26
|
+
*
|
|
27
|
+
* For React Native, ensure you have installed and imported the polyfill:
|
|
28
|
+
* - pnpm install react-native-get-random-values
|
|
29
|
+
* - Import at app entry point: import 'react-native-get-random-values';
|
|
30
|
+
*/
|
|
31
|
+
function generateUUID() {
|
|
32
|
+
// Try native crypto.randomUUID() first (modern browsers & Node.js 16.7+)
|
|
33
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
34
|
+
return crypto.randomUUID();
|
|
35
|
+
}
|
|
36
|
+
// Fallback using crypto.getRandomValues() (works with react-native-get-random-values polyfill)
|
|
37
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
38
|
+
const bytes = new Uint8Array(16);
|
|
39
|
+
crypto.getRandomValues(bytes);
|
|
40
|
+
// Set version (4) and variant (RFC 4122)
|
|
41
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
42
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
|
|
43
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
44
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
45
|
+
}
|
|
46
|
+
// Last resort fallback using Math.random() (not cryptographically secure)
|
|
47
|
+
console.warn('[Suparisma] crypto API not available, using Math.random() fallback for UUID generation');
|
|
48
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
49
|
+
const r = (Math.random() * 16) | 0;
|
|
50
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
51
|
+
return v.toString(16);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compare two values for sorting with proper type handling
|
|
56
|
+
*/
|
|
57
|
+
function compareValues(a, b, direction) {
|
|
58
|
+
// Handle undefined/null values
|
|
59
|
+
if (a === undefined || a === null)
|
|
60
|
+
return direction === 'asc' ? -1 : 1;
|
|
61
|
+
if (b === undefined || b === null)
|
|
62
|
+
return direction === 'asc' ? 1 : -1;
|
|
63
|
+
// Handle numbers properly to ensure numeric comparison
|
|
64
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
65
|
+
return direction === 'asc'
|
|
66
|
+
? a - b
|
|
67
|
+
: b - a;
|
|
68
|
+
}
|
|
69
|
+
// Handle dates (convert to timestamps for comparison)
|
|
70
|
+
if (a instanceof Date && b instanceof Date) {
|
|
71
|
+
return direction === 'asc'
|
|
72
|
+
? a.getTime() - b.getTime()
|
|
73
|
+
: b.getTime() - a.getTime();
|
|
74
|
+
}
|
|
75
|
+
// Handle strings or mixed types with string conversion
|
|
76
|
+
const aStr = String(a);
|
|
77
|
+
const bStr = String(b);
|
|
78
|
+
return direction === 'asc'
|
|
79
|
+
? aStr.localeCompare(bStr)
|
|
80
|
+
: bStr.localeCompare(aStr);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Convert a type-safe where filter to Supabase filter string
|
|
84
|
+
* Note: Complex OR/AND operations may not be fully supported in realtime filters
|
|
85
|
+
* and will fall back to client-side filtering
|
|
86
|
+
*/
|
|
87
|
+
function buildFilterString(where) {
|
|
88
|
+
if (!where)
|
|
89
|
+
return undefined;
|
|
90
|
+
const whereObj = where;
|
|
91
|
+
// Check for OR/AND operations - these are complex for realtime filters
|
|
92
|
+
if (whereObj.OR || whereObj.AND) {
|
|
93
|
+
console.log('⚠️ Complex OR/AND filters detected - realtime will use client-side filtering');
|
|
94
|
+
// For complex logical operations, we'll rely on client-side filtering
|
|
95
|
+
// Return undefined to indicate no database-level filter should be applied
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const filters = [];
|
|
99
|
+
for (const [key, value] of Object.entries(whereObj)) {
|
|
100
|
+
if (value !== undefined && key !== 'OR' && key !== 'AND') {
|
|
101
|
+
if (typeof value === 'object' && value !== null) {
|
|
102
|
+
// Handle advanced operators
|
|
103
|
+
const advancedOps = value;
|
|
104
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
105
|
+
filters.push(`${key}=eq.${advancedOps.equals}`);
|
|
106
|
+
}
|
|
107
|
+
if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
108
|
+
filters.push(`${key}=neq.${advancedOps.not}`);
|
|
109
|
+
}
|
|
110
|
+
if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
111
|
+
const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
|
|
112
|
+
filters.push(`${key}=gt.${value}`);
|
|
113
|
+
}
|
|
114
|
+
if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
115
|
+
const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
|
|
116
|
+
filters.push(`${key}=gte.${value}`);
|
|
117
|
+
}
|
|
118
|
+
if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
119
|
+
const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
|
|
120
|
+
filters.push(`${key}=lt.${value}`);
|
|
121
|
+
}
|
|
122
|
+
if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
123
|
+
const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
|
|
124
|
+
filters.push(`${key}=lte.${value}`);
|
|
125
|
+
}
|
|
126
|
+
if ('in' in advancedOps && advancedOps.in?.length) {
|
|
127
|
+
filters.push(`${key}=in.(${advancedOps.in.join(',')})`);
|
|
128
|
+
}
|
|
129
|
+
if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
130
|
+
filters.push(`${key}=ilike.*${advancedOps.contains}*`);
|
|
131
|
+
}
|
|
132
|
+
if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
133
|
+
filters.push(`${key}=ilike.${advancedOps.startsWith}%`);
|
|
134
|
+
}
|
|
135
|
+
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
136
|
+
filters.push(`${key}=ilike.%${advancedOps.endsWith}`);
|
|
137
|
+
}
|
|
138
|
+
// Array-specific operators
|
|
139
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
140
|
+
// Array contains ANY of the specified items (overlaps)
|
|
141
|
+
const arrayValue = JSON.stringify(advancedOps.has);
|
|
142
|
+
filters.push(`${key}=ov.${arrayValue}`);
|
|
143
|
+
}
|
|
144
|
+
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
145
|
+
// Array contains ALL of the specified items (contains)
|
|
146
|
+
const arrayValue = JSON.stringify(advancedOps.hasEvery);
|
|
147
|
+
filters.push(`${key}=cs.${arrayValue}`);
|
|
148
|
+
}
|
|
149
|
+
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
150
|
+
// Array contains ANY of the specified items (overlaps)
|
|
151
|
+
const arrayValue = JSON.stringify(advancedOps.hasSome);
|
|
152
|
+
filters.push(`${key}=ov.${arrayValue}`);
|
|
153
|
+
}
|
|
154
|
+
if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
155
|
+
if (advancedOps.isEmpty) {
|
|
156
|
+
// Check if array is empty
|
|
157
|
+
filters.push(`${key}=eq.{}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Check if array is not empty
|
|
161
|
+
filters.push(`${key}=neq.{}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Simple equality
|
|
167
|
+
filters.push(`${key}=eq.${value}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return filters.length > 0 ? filters.join(',') : undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Apply a single condition group to the query builder
|
|
175
|
+
*/
|
|
176
|
+
function applyConditionGroup(query, conditions) {
|
|
177
|
+
if (!conditions)
|
|
178
|
+
return query;
|
|
179
|
+
let filteredQuery = query;
|
|
180
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
181
|
+
if (value !== undefined && key !== 'OR' && key !== 'AND') {
|
|
182
|
+
if (typeof value === 'object' && value !== null) {
|
|
183
|
+
// Handle advanced operators
|
|
184
|
+
const advancedOps = value;
|
|
185
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
186
|
+
// @ts-ignore: Supabase typing issue
|
|
187
|
+
filteredQuery = filteredQuery.eq(key, advancedOps.equals);
|
|
188
|
+
}
|
|
189
|
+
if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
190
|
+
// @ts-ignore: Supabase typing issue
|
|
191
|
+
filteredQuery = filteredQuery.neq(key, advancedOps.not);
|
|
192
|
+
}
|
|
193
|
+
if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
194
|
+
// Convert Date objects to ISO strings for Supabase
|
|
195
|
+
const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
|
|
196
|
+
// @ts-ignore: Supabase typing issue
|
|
197
|
+
filteredQuery = filteredQuery.gt(key, value);
|
|
198
|
+
}
|
|
199
|
+
if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
200
|
+
// Convert Date objects to ISO strings for Supabase
|
|
201
|
+
const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
|
|
202
|
+
// @ts-ignore: Supabase typing issue
|
|
203
|
+
filteredQuery = filteredQuery.gte(key, value);
|
|
204
|
+
}
|
|
205
|
+
if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
206
|
+
// Convert Date objects to ISO strings for Supabase
|
|
207
|
+
const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
|
|
208
|
+
// @ts-ignore: Supabase typing issue
|
|
209
|
+
filteredQuery = filteredQuery.lt(key, value);
|
|
210
|
+
}
|
|
211
|
+
if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
212
|
+
// Convert Date objects to ISO strings for Supabase
|
|
213
|
+
const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
|
|
214
|
+
// @ts-ignore: Supabase typing issue
|
|
215
|
+
filteredQuery = filteredQuery.lte(key, value);
|
|
216
|
+
}
|
|
217
|
+
if ('in' in advancedOps && advancedOps.in?.length) {
|
|
218
|
+
// @ts-ignore: Supabase typing issue
|
|
219
|
+
filteredQuery = filteredQuery.in(key, advancedOps.in);
|
|
220
|
+
}
|
|
221
|
+
if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
222
|
+
// @ts-ignore: Supabase typing issue
|
|
223
|
+
filteredQuery = filteredQuery.ilike(key, `*${advancedOps.contains}*`);
|
|
224
|
+
}
|
|
225
|
+
if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
226
|
+
// @ts-ignore: Supabase typing issue
|
|
227
|
+
filteredQuery = filteredQuery.ilike(key, `${advancedOps.startsWith}%`);
|
|
228
|
+
}
|
|
229
|
+
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
230
|
+
// @ts-ignore: Supabase typing issue
|
|
231
|
+
filteredQuery = filteredQuery.ilike(key, `%${advancedOps.endsWith}`);
|
|
232
|
+
}
|
|
233
|
+
// Array-specific operators
|
|
234
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
235
|
+
// Array contains ANY of the specified items (overlaps)
|
|
236
|
+
// @ts-ignore: Supabase typing issue
|
|
237
|
+
filteredQuery = filteredQuery.overlaps(key, advancedOps.has);
|
|
238
|
+
}
|
|
239
|
+
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
240
|
+
// Array contains ALL of the specified items (contains)
|
|
241
|
+
// @ts-ignore: Supabase typing issue
|
|
242
|
+
filteredQuery = filteredQuery.contains(key, advancedOps.hasEvery);
|
|
243
|
+
}
|
|
244
|
+
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
245
|
+
// Array contains ANY of the specified items (overlaps)
|
|
246
|
+
// @ts-ignore: Supabase typing issue
|
|
247
|
+
filteredQuery = filteredQuery.overlaps(key, advancedOps.hasSome);
|
|
248
|
+
}
|
|
249
|
+
if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
250
|
+
if (advancedOps.isEmpty) {
|
|
251
|
+
// Check if array is empty
|
|
252
|
+
// @ts-ignore: Supabase typing issue
|
|
253
|
+
filteredQuery = filteredQuery.eq(key, []);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Check if array is not empty
|
|
257
|
+
// @ts-ignore: Supabase typing issue
|
|
258
|
+
filteredQuery = filteredQuery.neq(key, []);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Simple equality
|
|
264
|
+
// @ts-ignore: Supabase typing issue
|
|
265
|
+
filteredQuery = filteredQuery.eq(key, value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return filteredQuery;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Apply filter to the query builder with OR/AND support
|
|
273
|
+
*/
|
|
274
|
+
function applyFilter(query, where) {
|
|
275
|
+
if (!where)
|
|
276
|
+
return query;
|
|
277
|
+
const whereObj = where;
|
|
278
|
+
let filteredQuery = query;
|
|
279
|
+
// Handle regular conditions first (these are implicitly AND-ed)
|
|
280
|
+
filteredQuery = applyConditionGroup(filteredQuery, whereObj);
|
|
281
|
+
// Handle OR conditions
|
|
282
|
+
if (whereObj.OR && Array.isArray(whereObj.OR) && whereObj.OR.length > 0) {
|
|
283
|
+
// @ts-ignore: Supabase typing issue
|
|
284
|
+
filteredQuery = filteredQuery.or(whereObj.OR.map((orCondition, index) => {
|
|
285
|
+
// Convert each OR condition to a filter string
|
|
286
|
+
const orFilters = [];
|
|
287
|
+
for (const [key, value] of Object.entries(orCondition)) {
|
|
288
|
+
if (value !== undefined && key !== 'OR' && key !== 'AND') {
|
|
289
|
+
if (typeof value === 'object' && value !== null) {
|
|
290
|
+
const advancedOps = value;
|
|
291
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
292
|
+
orFilters.push(`${key}.eq.${advancedOps.equals}`);
|
|
293
|
+
}
|
|
294
|
+
else if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
295
|
+
orFilters.push(`${key}.neq.${advancedOps.not}`);
|
|
296
|
+
}
|
|
297
|
+
else if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
298
|
+
const value = advancedOps.gt instanceof Date ? advancedOps.gt.toISOString() : advancedOps.gt;
|
|
299
|
+
orFilters.push(`${key}.gt.${value}`);
|
|
300
|
+
}
|
|
301
|
+
else if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
302
|
+
const value = advancedOps.gte instanceof Date ? advancedOps.gte.toISOString() : advancedOps.gte;
|
|
303
|
+
orFilters.push(`${key}.gte.${value}`);
|
|
304
|
+
}
|
|
305
|
+
else if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
306
|
+
const value = advancedOps.lt instanceof Date ? advancedOps.lt.toISOString() : advancedOps.lt;
|
|
307
|
+
orFilters.push(`${key}.lt.${value}`);
|
|
308
|
+
}
|
|
309
|
+
else if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
310
|
+
const value = advancedOps.lte instanceof Date ? advancedOps.lte.toISOString() : advancedOps.lte;
|
|
311
|
+
orFilters.push(`${key}.lte.${value}`);
|
|
312
|
+
}
|
|
313
|
+
else if ('in' in advancedOps && advancedOps.in?.length) {
|
|
314
|
+
orFilters.push(`${key}.in.(${advancedOps.in.join(',')})`);
|
|
315
|
+
}
|
|
316
|
+
else if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
317
|
+
orFilters.push(`${key}.ilike.*${advancedOps.contains}*`);
|
|
318
|
+
}
|
|
319
|
+
else if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
320
|
+
orFilters.push(`${key}.ilike.${advancedOps.startsWith}%`);
|
|
321
|
+
}
|
|
322
|
+
else if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
323
|
+
orFilters.push(`${key}.ilike.%${advancedOps.endsWith}`);
|
|
324
|
+
}
|
|
325
|
+
else if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
326
|
+
orFilters.push(`${key}.ov.${JSON.stringify(advancedOps.has)}`);
|
|
327
|
+
}
|
|
328
|
+
else if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
329
|
+
orFilters.push(`${key}.cs.${JSON.stringify(advancedOps.hasEvery)}`);
|
|
330
|
+
}
|
|
331
|
+
else if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
332
|
+
orFilters.push(`${key}.ov.${JSON.stringify(advancedOps.hasSome)}`);
|
|
333
|
+
}
|
|
334
|
+
else if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
335
|
+
if (advancedOps.isEmpty) {
|
|
336
|
+
orFilters.push(`${key}.eq.{}`);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
orFilters.push(`${key}.neq.{}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
// Simple equality
|
|
345
|
+
orFilters.push(`${key}.eq.${value}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return orFilters.join(',');
|
|
350
|
+
}).join(','));
|
|
351
|
+
}
|
|
352
|
+
// Handle AND conditions (these are applied in addition to regular conditions)
|
|
353
|
+
if (whereObj.AND && Array.isArray(whereObj.AND) && whereObj.AND.length > 0) {
|
|
354
|
+
for (const andCondition of whereObj.AND) {
|
|
355
|
+
filteredQuery = applyConditionGroup(filteredQuery, andCondition);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return filteredQuery;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Evaluate if a record matches filter criteria (including OR/AND logic)
|
|
362
|
+
*/
|
|
363
|
+
function matchesFilter(record, filter) {
|
|
364
|
+
if (!filter)
|
|
365
|
+
return true;
|
|
366
|
+
const filterObj = filter;
|
|
367
|
+
// Separate regular conditions from OR/AND
|
|
368
|
+
const hasOr = filterObj.OR && Array.isArray(filterObj.OR) && filterObj.OR.length > 0;
|
|
369
|
+
const hasAnd = filterObj.AND && Array.isArray(filterObj.AND) && filterObj.AND.length > 0;
|
|
370
|
+
// Check regular field conditions (these are implicitly AND-ed)
|
|
371
|
+
const regularConditions = {};
|
|
372
|
+
for (const [key, value] of Object.entries(filterObj)) {
|
|
373
|
+
if (value !== undefined && key !== 'OR' && key !== 'AND') {
|
|
374
|
+
regularConditions[key] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Helper function to convert values to comparable format for date/time comparisons
|
|
378
|
+
const getComparableValue = (value) => {
|
|
379
|
+
if (value instanceof Date) {
|
|
380
|
+
return value.getTime();
|
|
381
|
+
}
|
|
382
|
+
if (typeof value === 'string' && value.match(/^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}/)) {
|
|
383
|
+
// ISO date string
|
|
384
|
+
return new Date(value).getTime();
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
};
|
|
388
|
+
// Helper function to check individual field conditions
|
|
389
|
+
const checkFieldConditions = (conditions) => {
|
|
390
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
391
|
+
if (value !== undefined) {
|
|
392
|
+
const recordValue = record[key];
|
|
393
|
+
if (typeof value === 'object' && value !== null) {
|
|
394
|
+
// Handle advanced operators
|
|
395
|
+
const advancedOps = value;
|
|
396
|
+
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
397
|
+
if (recordValue !== advancedOps.equals)
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
401
|
+
if (recordValue === advancedOps.not)
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
405
|
+
const recordComparable = getComparableValue(recordValue);
|
|
406
|
+
const filterComparable = getComparableValue(advancedOps.gt);
|
|
407
|
+
if (!(recordComparable > filterComparable))
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
411
|
+
const recordComparable = getComparableValue(recordValue);
|
|
412
|
+
const filterComparable = getComparableValue(advancedOps.gte);
|
|
413
|
+
if (!(recordComparable >= filterComparable))
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
417
|
+
const recordComparable = getComparableValue(recordValue);
|
|
418
|
+
const filterComparable = getComparableValue(advancedOps.lt);
|
|
419
|
+
if (!(recordComparable < filterComparable))
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
423
|
+
const recordComparable = getComparableValue(recordValue);
|
|
424
|
+
const filterComparable = getComparableValue(advancedOps.lte);
|
|
425
|
+
if (!(recordComparable <= filterComparable))
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if ('in' in advancedOps && advancedOps.in?.length) {
|
|
429
|
+
if (!advancedOps.in.includes(recordValue))
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
433
|
+
if (!recordValue || !String(recordValue).toLowerCase().includes(String(advancedOps.contains).toLowerCase()))
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
437
|
+
if (!recordValue || !String(recordValue).toLowerCase().startsWith(String(advancedOps.startsWith).toLowerCase()))
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
441
|
+
if (!recordValue || !String(recordValue).toLowerCase().endsWith(String(advancedOps.endsWith).toLowerCase()))
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
// Array-specific operators
|
|
445
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
446
|
+
if (!Array.isArray(recordValue) || !advancedOps.has.some((item) => recordValue.includes(item)))
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
450
|
+
if (!Array.isArray(recordValue) || !advancedOps.hasEvery.every((item) => recordValue.includes(item)))
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
454
|
+
if (!Array.isArray(recordValue) || !advancedOps.hasSome.some((item) => recordValue.includes(item)))
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
458
|
+
const isEmpty = !Array.isArray(recordValue) || recordValue.length === 0;
|
|
459
|
+
if (isEmpty !== advancedOps.isEmpty)
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// Simple equality
|
|
465
|
+
if (recordValue !== value)
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
};
|
|
472
|
+
// All conditions that must be true
|
|
473
|
+
const conditions = [];
|
|
474
|
+
// Regular field conditions (implicitly AND-ed)
|
|
475
|
+
if (Object.keys(regularConditions).length > 0) {
|
|
476
|
+
conditions.push(checkFieldConditions(regularConditions));
|
|
477
|
+
}
|
|
478
|
+
// AND conditions (all must be true)
|
|
479
|
+
if (hasAnd) {
|
|
480
|
+
const andResult = filterObj.AND.every((andCondition) => matchesFilter(record, andCondition));
|
|
481
|
+
conditions.push(andResult);
|
|
482
|
+
}
|
|
483
|
+
// OR conditions (at least one must be true)
|
|
484
|
+
if (hasOr) {
|
|
485
|
+
const orResult = filterObj.OR.some((orCondition) => matchesFilter(record, orCondition));
|
|
486
|
+
conditions.push(orResult);
|
|
487
|
+
}
|
|
488
|
+
// All conditions must be true
|
|
489
|
+
return conditions.every(condition => condition);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Build a Supabase select string from select and include options.
|
|
493
|
+
*
|
|
494
|
+
* @param select - Object specifying which fields to select { field: true }
|
|
495
|
+
* @param include - Object specifying which relations to include { relation: true }
|
|
496
|
+
* @returns A Supabase-compatible select string
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* // Select specific fields
|
|
500
|
+
* buildSelectString({ id: true, name: true }) // Returns "id,name"
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* // Include relations
|
|
504
|
+
* buildSelectString(undefined, { posts: true }) // Returns "*,posts(*)"
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* // Select fields and include relations with specific fields
|
|
508
|
+
* buildSelectString({ id: true, name: true }, { posts: { select: { id: true, title: true } } })
|
|
509
|
+
* // Returns "id,name,posts(id,title)"
|
|
510
|
+
*/
|
|
511
|
+
function buildSelectString(select, include, relationMappings) {
|
|
512
|
+
const parts = [];
|
|
513
|
+
// Handle select - if provided, only return specified fields
|
|
514
|
+
if (select && typeof select === 'object') {
|
|
515
|
+
const selectedFields = Object.entries(select)
|
|
516
|
+
.filter(([_, value]) => value === true)
|
|
517
|
+
.map(([key]) => key);
|
|
518
|
+
if (selectedFields.length > 0) {
|
|
519
|
+
parts.push(...selectedFields);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Handle include - add related records
|
|
523
|
+
if (include && typeof include === 'object') {
|
|
524
|
+
for (const [relationName, relationValue] of Object.entries(include)) {
|
|
525
|
+
const relatedTableName = relationMappings?.[relationName] || relationName;
|
|
526
|
+
// If mapping exists, use PostgREST alias syntax: alias:foreignTable(...)
|
|
527
|
+
const embedName = relationMappings?.[relationName] && relatedTableName !== relationName
|
|
528
|
+
? `${relationName}:${relatedTableName}`
|
|
529
|
+
: relationName;
|
|
530
|
+
if (relationValue === true) {
|
|
531
|
+
// Include all fields from the relation
|
|
532
|
+
parts.push(`${embedName}(*)`);
|
|
533
|
+
}
|
|
534
|
+
else if (typeof relationValue === 'object' && relationValue !== null) {
|
|
535
|
+
// Include specific fields from the relation
|
|
536
|
+
const relationOptions = relationValue;
|
|
537
|
+
if (relationOptions.select) {
|
|
538
|
+
const relationFields = Object.entries(relationOptions.select)
|
|
539
|
+
.filter(([_, value]) => value === true)
|
|
540
|
+
.map(([key]) => key);
|
|
541
|
+
if (relationFields.length > 0) {
|
|
542
|
+
parts.push(`${embedName}(${relationFields.join(',')})`);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
parts.push(`${embedName}(*)`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
parts.push(`${embedName}(*)`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// If no select specified but include is, we need to include base table fields too
|
|
555
|
+
if (parts.length === 0) {
|
|
556
|
+
return '*';
|
|
557
|
+
}
|
|
558
|
+
// If only include was specified (no select), we need all base fields plus relations
|
|
559
|
+
if (!select && include) {
|
|
560
|
+
return '*,' + parts.join(',');
|
|
561
|
+
}
|
|
562
|
+
return parts.join(',');
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Apply order by to the query builder
|
|
566
|
+
*/
|
|
567
|
+
function applyOrderBy(query, orderBy, hasCreatedAt, createdAtField = 'createdAt') {
|
|
568
|
+
if (!orderBy) {
|
|
569
|
+
// By default, sort by createdAt if available, using the actual field name from Prisma
|
|
570
|
+
if (hasCreatedAt) {
|
|
571
|
+
// @ts-ignore: Supabase typing issue
|
|
572
|
+
return query.order(createdAtField, { ascending: false });
|
|
573
|
+
}
|
|
574
|
+
return query;
|
|
575
|
+
}
|
|
576
|
+
// Apply each order by clause
|
|
577
|
+
let orderedQuery = query;
|
|
578
|
+
// Handle orderBy as array or single object
|
|
579
|
+
const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
580
|
+
for (const orderByClause of orderByArray) {
|
|
581
|
+
for (const [key, direction] of Object.entries(orderByClause)) {
|
|
582
|
+
// @ts-ignore: Supabase typing issue
|
|
583
|
+
orderedQuery = orderedQuery.order(key, {
|
|
584
|
+
ascending: direction === 'asc'
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return orderedQuery;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Core hook factory function that creates a type-safe realtime hook for a specific model.
|
|
592
|
+
* This is the foundation for all Suparisma hooks.
|
|
593
|
+
*/
|
|
594
|
+
function createSuparismaHook(config) {
|
|
595
|
+
const { tableName, hasCreatedAt, hasUpdatedAt, searchFields = [], defaultValues = {}, createdAtField = 'createdAt', updatedAtField = 'updatedAt', relationMappings = {} } = config;
|
|
596
|
+
/**
|
|
597
|
+
* The main hook function that provides all data access methods for a model.
|
|
598
|
+
*
|
|
599
|
+
* @param options - Optional configuration for data fetching, filtering, and realtime
|
|
600
|
+
*
|
|
601
|
+
* @returns An API object with data state and CRUD methods
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* // Basic usage
|
|
605
|
+
* const users = useSuparismaUser();
|
|
606
|
+
* const { data, loading, error } = users;
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* // With filtering
|
|
610
|
+
* const users = useSuparismaUser({
|
|
611
|
+
* where: { role: 'admin' },
|
|
612
|
+
* orderBy: { created_at: 'desc' }
|
|
613
|
+
* });
|
|
614
|
+
*/
|
|
615
|
+
return function useSuparismaHook(options = {}) {
|
|
616
|
+
const { realtime = true, channelName, where, realtimeFilter, orderBy, limit, offset, select, include, enabled = true, } = options;
|
|
617
|
+
// Build the select string once for reuse
|
|
618
|
+
const selectString = buildSelectString(select, include, relationMappings);
|
|
619
|
+
// Refs to store the latest options for realtime handlers
|
|
620
|
+
const whereRef = (0, react_1.useRef)(where);
|
|
621
|
+
const orderByRef = (0, react_1.useRef)(orderBy);
|
|
622
|
+
const limitRef = (0, react_1.useRef)(limit);
|
|
623
|
+
const offsetRef = (0, react_1.useRef)(offset);
|
|
624
|
+
const selectStringRef = (0, react_1.useRef)(selectString);
|
|
625
|
+
const enabledRef = (0, react_1.useRef)(enabled);
|
|
626
|
+
// Update refs whenever options change
|
|
627
|
+
(0, react_1.useEffect)(() => {
|
|
628
|
+
whereRef.current = where;
|
|
629
|
+
}, [where]);
|
|
630
|
+
(0, react_1.useEffect)(() => {
|
|
631
|
+
orderByRef.current = orderBy;
|
|
632
|
+
}, [orderBy]);
|
|
633
|
+
(0, react_1.useEffect)(() => {
|
|
634
|
+
limitRef.current = limit;
|
|
635
|
+
}, [limit]);
|
|
636
|
+
(0, react_1.useEffect)(() => {
|
|
637
|
+
offsetRef.current = offset;
|
|
638
|
+
}, [offset]);
|
|
639
|
+
(0, react_1.useEffect)(() => {
|
|
640
|
+
selectStringRef.current = selectString;
|
|
641
|
+
}, [selectString]);
|
|
642
|
+
(0, react_1.useEffect)(() => {
|
|
643
|
+
enabledRef.current = enabled;
|
|
644
|
+
}, [enabled]);
|
|
645
|
+
// Single data collection for holding results
|
|
646
|
+
const [data, setData] = (0, react_1.useState)([]);
|
|
647
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
648
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
649
|
+
// This is the total count, unaffected by pagination limits
|
|
650
|
+
const [count, setCount] = (0, react_1.useState)(0);
|
|
651
|
+
// Search state
|
|
652
|
+
const [searchQueries, setSearchQueries] = (0, react_1.useState)([]);
|
|
653
|
+
const [searchLoading, setSearchLoading] = (0, react_1.useState)(false);
|
|
654
|
+
const initialLoadRef = (0, react_1.useRef)(false);
|
|
655
|
+
const channelRef = (0, react_1.useRef)(null);
|
|
656
|
+
const searchTimeoutRef = (0, react_1.useRef)(null);
|
|
657
|
+
const isSearchingRef = (0, react_1.useRef)(false);
|
|
658
|
+
// Function to fetch the total count from Supabase with current filters.
|
|
659
|
+
// IMPORTANT: do NOT capture unstable objects (where/orderBy/etc) in deps.
|
|
660
|
+
// Read the latest values from refs to avoid effect→setState→rerender loops.
|
|
661
|
+
const fetchTotalCount = (0, react_1.useCallback)(async () => {
|
|
662
|
+
try {
|
|
663
|
+
// Skip count updates when disabled or during search
|
|
664
|
+
if (!enabledRef.current)
|
|
665
|
+
return;
|
|
666
|
+
if (isSearchingRef.current)
|
|
667
|
+
return;
|
|
668
|
+
let countQuery = supabase_client_1.supabase.from(tableName).select('*', { count: 'exact', head: true });
|
|
669
|
+
// Apply current where conditions via ref (NOT the captured 'where')
|
|
670
|
+
const currentWhere = whereRef.current;
|
|
671
|
+
if (currentWhere) {
|
|
672
|
+
countQuery = applyFilter(countQuery, currentWhere);
|
|
673
|
+
}
|
|
674
|
+
const { count: totalCount, error: countError } = await countQuery;
|
|
675
|
+
if (!countError) {
|
|
676
|
+
const nextCount = totalCount || 0;
|
|
677
|
+
// Cheap guard to reduce churn
|
|
678
|
+
setCount((prev) => (prev === nextCount ? prev : nextCount));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
console.error(`Error fetching count for ${tableName}:`, err);
|
|
683
|
+
}
|
|
684
|
+
}, [tableName]);
|
|
685
|
+
// Create the search state object with all required methods
|
|
686
|
+
const search = {
|
|
687
|
+
queries: searchQueries,
|
|
688
|
+
loading: searchLoading,
|
|
689
|
+
// Set all search queries at once
|
|
690
|
+
setQueries: (0, react_1.useCallback)((queries) => {
|
|
691
|
+
// Validate that all fields are searchable
|
|
692
|
+
const validQueries = queries.filter(query => searchFields.includes(query.field) && query.value.trim() !== '');
|
|
693
|
+
setSearchQueries(validQueries);
|
|
694
|
+
// Execute search if there are valid queries
|
|
695
|
+
if (validQueries.length > 0) {
|
|
696
|
+
executeSearch(validQueries);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
// If no valid queries, reset to normal data fetching
|
|
700
|
+
isSearchingRef.current = false;
|
|
701
|
+
findMany({ where, orderBy, take: limit, skip: offset });
|
|
702
|
+
}
|
|
703
|
+
}, [where, orderBy, limit, offset]),
|
|
704
|
+
// Add a single search query
|
|
705
|
+
addQuery: (0, react_1.useCallback)((query) => {
|
|
706
|
+
// Validate that the field is searchable
|
|
707
|
+
if (!searchFields.includes(query.field) || query.value.trim() === '') {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
setSearchQueries(prev => {
|
|
711
|
+
// Replace if query for this field already exists, otherwise add
|
|
712
|
+
const exists = prev.some(q => q.field === query.field);
|
|
713
|
+
const newQueries = exists
|
|
714
|
+
? prev.map(q => q.field === query.field ? query : q)
|
|
715
|
+
: [...prev, query];
|
|
716
|
+
// Execute search with updated queries
|
|
717
|
+
executeSearch(newQueries);
|
|
718
|
+
return newQueries;
|
|
719
|
+
});
|
|
720
|
+
}, []),
|
|
721
|
+
// Remove a search query by field
|
|
722
|
+
removeQuery: (0, react_1.useCallback)((field) => {
|
|
723
|
+
setSearchQueries(prev => {
|
|
724
|
+
const newQueries = prev.filter(q => q.field !== field);
|
|
725
|
+
// If we still have queries, execute search with remaining queries
|
|
726
|
+
if (newQueries.length > 0) {
|
|
727
|
+
executeSearch(newQueries);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
// If no queries left, reset to normal data fetching
|
|
731
|
+
isSearchingRef.current = false;
|
|
732
|
+
findMany({ where, orderBy, take: limit, skip: offset });
|
|
733
|
+
}
|
|
734
|
+
return newQueries;
|
|
735
|
+
});
|
|
736
|
+
}, [where, orderBy, limit, offset]),
|
|
737
|
+
// Clear all search queries
|
|
738
|
+
clearQueries: (0, react_1.useCallback)(() => {
|
|
739
|
+
setSearchQueries([]);
|
|
740
|
+
isSearchingRef.current = false;
|
|
741
|
+
findMany({ where, orderBy, take: limit, skip: offset });
|
|
742
|
+
}, [where, orderBy, limit, offset]),
|
|
743
|
+
// Search across multiple fields (convenience method)
|
|
744
|
+
searchMultiField: (0, react_1.useCallback)((value) => {
|
|
745
|
+
if (searchFields.length <= 1) {
|
|
746
|
+
console.warn('Multi-field search requires at least 2 searchable fields');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
setSearchQueries([{ field: 'multi', value }]);
|
|
750
|
+
executeSearch([{ field: 'multi', value }]);
|
|
751
|
+
}, [searchFields.length]),
|
|
752
|
+
// Search in a specific field (convenience method)
|
|
753
|
+
searchField: (0, react_1.useCallback)((field, value) => {
|
|
754
|
+
if (!searchFields.includes(field)) {
|
|
755
|
+
console.warn(`Field "${field}" is not searchable. Available fields: ${searchFields.join(', ')}`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
setSearchQueries([{ field, value }]);
|
|
759
|
+
executeSearch([{ field, value }]);
|
|
760
|
+
}, [searchFields]),
|
|
761
|
+
// Get current search terms for custom highlighting
|
|
762
|
+
getCurrentSearchTerms: (0, react_1.useCallback)(() => {
|
|
763
|
+
return searchQueries.map(q => q.value.trim());
|
|
764
|
+
}, [searchQueries]),
|
|
765
|
+
// Safely escape regex special characters
|
|
766
|
+
escapeRegex: (0, react_1.useCallback)((text) => {
|
|
767
|
+
return escapeRegexCharacters(text);
|
|
768
|
+
}, [])
|
|
769
|
+
};
|
|
770
|
+
// Execute search based on queries
|
|
771
|
+
const executeSearch = (0, react_1.useCallback)(async (queries) => {
|
|
772
|
+
// Clear the previous timeout
|
|
773
|
+
if (searchTimeoutRef.current) {
|
|
774
|
+
clearTimeout(searchTimeoutRef.current);
|
|
775
|
+
}
|
|
776
|
+
// Skip if no searchable fields or no valid queries
|
|
777
|
+
if (searchFields.length === 0 || queries.length === 0) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
setSearchLoading(true);
|
|
781
|
+
isSearchingRef.current = true;
|
|
782
|
+
// Use debounce to prevent rapid searches
|
|
783
|
+
searchTimeoutRef.current = setTimeout(async () => {
|
|
784
|
+
try {
|
|
785
|
+
let results = [];
|
|
786
|
+
// Validate search queries
|
|
787
|
+
const validQueries = queries.filter(query => {
|
|
788
|
+
if (!query.field || !query.value) {
|
|
789
|
+
console.warn('Invalid search query - missing field or value:', query);
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
// Allow "multi" as a special field for multi-field search
|
|
793
|
+
if (query.field === 'multi' && searchFields.length > 1) {
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
if (!searchFields.includes(query.field)) {
|
|
797
|
+
console.warn(`Field "${query.field}" is not searchable. Available fields: ${searchFields.join(', ')}, or "multi" for multi-field search`);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
return true;
|
|
801
|
+
});
|
|
802
|
+
if (validQueries.length === 0) {
|
|
803
|
+
console.log('No valid search queries found');
|
|
804
|
+
setData([]);
|
|
805
|
+
setCount(0);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// Execute RPC function for each query using Promise.all
|
|
809
|
+
const searchPromises = validQueries.map(query => {
|
|
810
|
+
// Build function name based on field type
|
|
811
|
+
const functionName = query.field === 'multi'
|
|
812
|
+
? `search_${tableName.toLowerCase()}_multi_field`
|
|
813
|
+
: `search_${tableName.toLowerCase()}_by_${query.field.toLowerCase()}_prefix`;
|
|
814
|
+
console.log(`🔍 Executing search: ${functionName}(search_prefix: "${query.value.trim()}")`);
|
|
815
|
+
// Call RPC function with proper error handling
|
|
816
|
+
return Promise.resolve(supabase_client_1.supabase.rpc(functionName, { search_prefix: query.value.trim() }))
|
|
817
|
+
.then((result) => ({
|
|
818
|
+
...result,
|
|
819
|
+
queryField: query.field,
|
|
820
|
+
queryValue: query.value
|
|
821
|
+
}))
|
|
822
|
+
.catch((error) => ({
|
|
823
|
+
data: null,
|
|
824
|
+
error: error,
|
|
825
|
+
queryField: query.field,
|
|
826
|
+
queryValue: query.value
|
|
827
|
+
}));
|
|
828
|
+
});
|
|
829
|
+
// Execute all search queries in parallel
|
|
830
|
+
const searchResults = await Promise.all(searchPromises);
|
|
831
|
+
// Combine and deduplicate results
|
|
832
|
+
const allResults = {};
|
|
833
|
+
let hasErrors = false;
|
|
834
|
+
// Process each search result
|
|
835
|
+
searchResults.forEach((result, index) => {
|
|
836
|
+
if (result.error) {
|
|
837
|
+
console.error(`🔍 Search error for field "${result.queryField}" with value "${result.queryValue}":`, result.error);
|
|
838
|
+
hasErrors = true;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (result.data && Array.isArray(result.data)) {
|
|
842
|
+
console.log(`🔍 Search results for "${result.queryField}": ${result.data.length} items`);
|
|
843
|
+
// Add each result, using id as key to deduplicate
|
|
844
|
+
for (const item of result.data) {
|
|
845
|
+
// @ts-ignore: Assume item has an id property
|
|
846
|
+
if (item && typeof item === 'object' && 'id' in item && item.id) {
|
|
847
|
+
// @ts-ignore: Add to results using id as key
|
|
848
|
+
allResults[item.id] = item;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
else if (result.data) {
|
|
853
|
+
console.warn(`🔍 Unexpected search result format for "${result.queryField}":`, typeof result.data);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
// Convert back to array
|
|
857
|
+
results = Object.values(allResults);
|
|
858
|
+
console.log(`🔍 Combined search results: ${results.length} unique items`);
|
|
859
|
+
// Apply any where conditions client-side (now using the proper filter function)
|
|
860
|
+
if (where) {
|
|
861
|
+
const originalCount = results.length;
|
|
862
|
+
results = results.filter((item) => matchesFilter(item, where));
|
|
863
|
+
console.log(`🔍 After applying where filter: ${results.length}/${originalCount} items`);
|
|
864
|
+
}
|
|
865
|
+
// Set count directly for search results
|
|
866
|
+
setCount(results.length);
|
|
867
|
+
// Apply ordering if needed (using the proper compare function)
|
|
868
|
+
if (orderBy) {
|
|
869
|
+
const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
870
|
+
results = [...results].sort((a, b) => {
|
|
871
|
+
for (const orderByClause of orderByArray) {
|
|
872
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
873
|
+
const aValue = a[field];
|
|
874
|
+
const bValue = b[field];
|
|
875
|
+
if (aValue === bValue)
|
|
876
|
+
continue;
|
|
877
|
+
return compareValues(aValue, bValue, direction);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return 0;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
// Apply pagination if needed
|
|
884
|
+
let paginatedResults = results;
|
|
885
|
+
if (offset && offset > 0) {
|
|
886
|
+
paginatedResults = paginatedResults.slice(offset);
|
|
887
|
+
}
|
|
888
|
+
if (limit && limit > 0) {
|
|
889
|
+
paginatedResults = paginatedResults.slice(0, limit);
|
|
890
|
+
}
|
|
891
|
+
console.log(`🔍 Final search results: ${paginatedResults.length} items (total: ${results.length})`);
|
|
892
|
+
// Update data with search results
|
|
893
|
+
setData(paginatedResults);
|
|
894
|
+
// Show error if there were issues but still show partial results
|
|
895
|
+
if (hasErrors && results.length === 0) {
|
|
896
|
+
setError(new Error('Search failed - please check if search functions are properly configured'));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
console.error('🔍 Search error:', err);
|
|
901
|
+
setError(err);
|
|
902
|
+
setData([]);
|
|
903
|
+
setCount(0);
|
|
904
|
+
}
|
|
905
|
+
finally {
|
|
906
|
+
setSearchLoading(false);
|
|
907
|
+
}
|
|
908
|
+
}, 300); // 300ms debounce
|
|
909
|
+
}, [tableName, searchFields, where, orderBy, limit, offset]);
|
|
910
|
+
/**
|
|
911
|
+
* Fetch multiple records with support for filtering, sorting, and pagination.
|
|
912
|
+
*
|
|
913
|
+
* @param params - Query parameters for filtering and ordering records
|
|
914
|
+
* @returns A promise with the fetched data or error
|
|
915
|
+
*
|
|
916
|
+
* @example
|
|
917
|
+
* // Get all active users
|
|
918
|
+
* const result = await users.findMany({
|
|
919
|
+
* where: { active: true }
|
|
920
|
+
* });
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* // Get 10 most recent posts with pagination
|
|
924
|
+
* const page1 = await posts.findMany({
|
|
925
|
+
* orderBy: { created_at: 'desc' },
|
|
926
|
+
* take: 10,
|
|
927
|
+
* skip: 0
|
|
928
|
+
* });
|
|
929
|
+
*
|
|
930
|
+
* const page2 = await posts.findMany({
|
|
931
|
+
* orderBy: { created_at: 'desc' },
|
|
932
|
+
* take: 10,
|
|
933
|
+
* skip: 10
|
|
934
|
+
* });
|
|
935
|
+
*/
|
|
936
|
+
const findMany = (0, react_1.useCallback)(async (params) => {
|
|
937
|
+
try {
|
|
938
|
+
setLoading(true);
|
|
939
|
+
setError(null);
|
|
940
|
+
// Use selectString for field selection (includes relations if specified)
|
|
941
|
+
let query = supabase_client_1.supabase.from(tableName).select(selectString);
|
|
942
|
+
// Apply where conditions if provided
|
|
943
|
+
if (params?.where) {
|
|
944
|
+
query = applyFilter(query, params.where);
|
|
945
|
+
}
|
|
946
|
+
// Apply order by if provided
|
|
947
|
+
if (params?.orderBy) {
|
|
948
|
+
query = applyOrderBy(query, params.orderBy, hasCreatedAt, createdAtField);
|
|
949
|
+
}
|
|
950
|
+
else if (hasCreatedAt) {
|
|
951
|
+
// Use the actual createdAt field name from Prisma
|
|
952
|
+
// @ts-ignore: Supabase typing issue
|
|
953
|
+
query = query.order(createdAtField, { ascending: false });
|
|
954
|
+
}
|
|
955
|
+
// Apply limit if provided
|
|
956
|
+
if (params?.take) {
|
|
957
|
+
query = query.limit(params.take);
|
|
958
|
+
}
|
|
959
|
+
// Apply offset if provided (for pagination)
|
|
960
|
+
if (params?.skip !== undefined && params.skip >= 0) {
|
|
961
|
+
query = query.range(params.skip, params.skip + (params.take || 10) - 1);
|
|
962
|
+
}
|
|
963
|
+
const { data, error } = await query;
|
|
964
|
+
if (error)
|
|
965
|
+
throw error;
|
|
966
|
+
const typedData = (data || []);
|
|
967
|
+
// Only update data if not currently searching
|
|
968
|
+
if (!isSearchingRef.current) {
|
|
969
|
+
setData(typedData);
|
|
970
|
+
// If the where filter changed, update the total count
|
|
971
|
+
if (JSON.stringify(params?.where) !== JSON.stringify(where)) {
|
|
972
|
+
// Use our standard count fetching function instead of duplicating logic
|
|
973
|
+
setTimeout(() => fetchTotalCount(), 0);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return { data: typedData, error: null };
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
console.error('Error finding records:', err);
|
|
980
|
+
setError(err);
|
|
981
|
+
return { data: null, error: err };
|
|
982
|
+
}
|
|
983
|
+
finally {
|
|
984
|
+
setLoading(false);
|
|
985
|
+
}
|
|
986
|
+
}, [fetchTotalCount, where, tableName, hasCreatedAt, createdAtField]);
|
|
987
|
+
/**
|
|
988
|
+
* Find a single record by its unique identifier (usually ID).
|
|
989
|
+
*
|
|
990
|
+
* @param where - The unique identifier to find the record by
|
|
991
|
+
* @returns A promise with the found record or error
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* // Find user by ID
|
|
995
|
+
* const result = await users.findUnique({ id: "123" });
|
|
996
|
+
* if (result.data) {
|
|
997
|
+
* console.log("Found user:", result.data.name);
|
|
998
|
+
* }
|
|
999
|
+
*/
|
|
1000
|
+
const findUnique = (0, react_1.useCallback)(async (where) => {
|
|
1001
|
+
try {
|
|
1002
|
+
setLoading(true);
|
|
1003
|
+
setError(null);
|
|
1004
|
+
// Find the primary field (usually 'id')
|
|
1005
|
+
// @ts-ignore: Supabase typing issue
|
|
1006
|
+
const primaryKey = Object.keys(where)[0];
|
|
1007
|
+
if (!primaryKey) {
|
|
1008
|
+
throw new Error('A unique identifier is required');
|
|
1009
|
+
}
|
|
1010
|
+
const value = where[primaryKey];
|
|
1011
|
+
if (value === undefined) {
|
|
1012
|
+
throw new Error('A unique identifier is required');
|
|
1013
|
+
}
|
|
1014
|
+
const { data, error } = await supabase_client_1.supabase
|
|
1015
|
+
.from(tableName)
|
|
1016
|
+
.select(selectString)
|
|
1017
|
+
.eq(primaryKey, value)
|
|
1018
|
+
.maybeSingle();
|
|
1019
|
+
if (error)
|
|
1020
|
+
throw error;
|
|
1021
|
+
return { data: data, error: null };
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
console.error('Error finding unique record:', err);
|
|
1025
|
+
setError(err);
|
|
1026
|
+
return { data: null, error: err };
|
|
1027
|
+
}
|
|
1028
|
+
finally {
|
|
1029
|
+
setLoading(false);
|
|
1030
|
+
}
|
|
1031
|
+
}, []);
|
|
1032
|
+
// Set up realtime subscription for the list - ONCE and listen to ALL events
|
|
1033
|
+
const channelIdRef = (0, react_1.useRef)(null);
|
|
1034
|
+
(0, react_1.useEffect)(() => {
|
|
1035
|
+
// Skip subscription if not enabled or realtime is off
|
|
1036
|
+
if (!enabled || !realtime)
|
|
1037
|
+
return;
|
|
1038
|
+
// Stable channel id per hook instance (unless user explicitly provides channelName)
|
|
1039
|
+
const channelId = channelName ??
|
|
1040
|
+
channelIdRef.current ??
|
|
1041
|
+
(channelIdRef.current = `changes_to_${tableName}_${generateUUID()}`);
|
|
1042
|
+
// ALWAYS listen to ALL events and filter client-side for maximum reliability
|
|
1043
|
+
let subscriptionConfig = {
|
|
1044
|
+
event: '*',
|
|
1045
|
+
schema: 'public',
|
|
1046
|
+
table: tableName,
|
|
1047
|
+
};
|
|
1048
|
+
console.log(`Setting up subscription for ${tableName} - listening to ALL events (client-side filtering)`);
|
|
1049
|
+
const channel = supabase_client_1.supabase
|
|
1050
|
+
.channel(channelId)
|
|
1051
|
+
.on('postgres_changes', subscriptionConfig, (payload) => {
|
|
1052
|
+
console.log(`🔥 REALTIME EVENT RECEIVED for ${tableName}:`, payload.eventType, payload);
|
|
1053
|
+
// Access current options via refs inside the event handler
|
|
1054
|
+
const currentWhere = whereRef.current;
|
|
1055
|
+
const currentOrderBy = orderByRef.current;
|
|
1056
|
+
const currentLimit = limitRef.current;
|
|
1057
|
+
const currentOffset = offsetRef.current; // Not directly used in handlers but good for consistency
|
|
1058
|
+
// Skip realtime updates when search is active
|
|
1059
|
+
if (isSearchingRef.current) {
|
|
1060
|
+
console.log('⏭️ Skipping realtime update - search is active');
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (payload.eventType === 'INSERT') {
|
|
1064
|
+
// Process insert event
|
|
1065
|
+
setData((prev) => {
|
|
1066
|
+
try {
|
|
1067
|
+
const newRecord = payload.new;
|
|
1068
|
+
console.log(`Processing INSERT for ${tableName}`, { newRecord });
|
|
1069
|
+
// ALWAYS check if this record matches our filter client-side
|
|
1070
|
+
// This is especially important for complex OR/AND/array filters
|
|
1071
|
+
if (currentWhere && !matchesFilter(newRecord, currentWhere)) {
|
|
1072
|
+
console.log('New record does not match filter criteria, skipping');
|
|
1073
|
+
return prev;
|
|
1074
|
+
}
|
|
1075
|
+
// Check if record already exists (avoid duplicates)
|
|
1076
|
+
const exists = prev.some(item =>
|
|
1077
|
+
// @ts-ignore: Supabase typing issue
|
|
1078
|
+
'id' in item && 'id' in newRecord && item.id === newRecord.id);
|
|
1079
|
+
if (exists) {
|
|
1080
|
+
console.log('Record already exists, skipping insertion');
|
|
1081
|
+
return prev;
|
|
1082
|
+
}
|
|
1083
|
+
// Add the new record to the data
|
|
1084
|
+
let newData = [...prev, newRecord]; // Changed: Use spread on prev for immutability
|
|
1085
|
+
// Apply ordering if specified
|
|
1086
|
+
if (currentOrderBy) { // Use ref value
|
|
1087
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
1088
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
1089
|
+
? currentOrderBy
|
|
1090
|
+
: [currentOrderBy];
|
|
1091
|
+
// Apply each sort in sequence
|
|
1092
|
+
newData = [...newData].sort((a, b) => {
|
|
1093
|
+
// Check each orderBy clause in sequence
|
|
1094
|
+
for (const orderByClause of orderByArray) {
|
|
1095
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
1096
|
+
const aValue = a[field];
|
|
1097
|
+
const bValue = b[field];
|
|
1098
|
+
// Skip if values are equal and move to next criterion
|
|
1099
|
+
if (aValue === bValue)
|
|
1100
|
+
continue;
|
|
1101
|
+
// Use the compareValues function for proper type handling
|
|
1102
|
+
return compareValues(aValue, bValue, direction);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return 0; // Equal if all criteria match
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
else if (hasCreatedAt) {
|
|
1109
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
1110
|
+
newData = [...newData].sort((a, b) => {
|
|
1111
|
+
const aValue = a[createdAtField];
|
|
1112
|
+
const bValue = b[createdAtField];
|
|
1113
|
+
return compareValues(aValue, bValue, 'desc');
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
// Apply limit if specified
|
|
1117
|
+
if (currentLimit && currentLimit > 0) { // Use ref value
|
|
1118
|
+
newData = newData.slice(0, currentLimit);
|
|
1119
|
+
}
|
|
1120
|
+
// Fetch the updated count after the data changes
|
|
1121
|
+
setTimeout(() => fetchTotalCount(), 0);
|
|
1122
|
+
return newData;
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
console.error('Error processing INSERT event:', error);
|
|
1126
|
+
return prev;
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
else if (payload.eventType === 'UPDATE') {
|
|
1131
|
+
// Process update event
|
|
1132
|
+
setData((prev) => {
|
|
1133
|
+
// Access current options via refs
|
|
1134
|
+
const currentOrderBy = orderByRef.current;
|
|
1135
|
+
const currentLimit = limitRef.current; // If needed for re-fetch logic on update
|
|
1136
|
+
const currentWhere = whereRef.current;
|
|
1137
|
+
// Skip if search is active
|
|
1138
|
+
if (isSearchingRef.current) {
|
|
1139
|
+
return prev;
|
|
1140
|
+
}
|
|
1141
|
+
const updatedRecord = payload.new;
|
|
1142
|
+
// Check if the updated record still matches our current filter
|
|
1143
|
+
if (currentWhere && !matchesFilter(updatedRecord, currentWhere)) {
|
|
1144
|
+
console.log('Updated record no longer matches filter, removing from list');
|
|
1145
|
+
return prev.filter((item) =>
|
|
1146
|
+
// @ts-ignore: Supabase typing issue
|
|
1147
|
+
!('id' in item && 'id' in updatedRecord && item.id === updatedRecord.id));
|
|
1148
|
+
}
|
|
1149
|
+
const newData = prev.map((item) =>
|
|
1150
|
+
// @ts-ignore: Supabase typing issue
|
|
1151
|
+
'id' in item && 'id' in payload.new && item.id === payload.new.id
|
|
1152
|
+
? payload.new
|
|
1153
|
+
: item);
|
|
1154
|
+
// Apply ordering again after update to ensure consistency
|
|
1155
|
+
let sortedData = [...newData];
|
|
1156
|
+
// Apply ordering if specified
|
|
1157
|
+
if (currentOrderBy) { // Use ref value
|
|
1158
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
1159
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
1160
|
+
? currentOrderBy
|
|
1161
|
+
: [currentOrderBy];
|
|
1162
|
+
// Apply each sort in sequence
|
|
1163
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1164
|
+
// Check each orderBy clause in sequence
|
|
1165
|
+
for (const orderByClause of orderByArray) {
|
|
1166
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
1167
|
+
const aValue = a[field];
|
|
1168
|
+
const bValue = b[field];
|
|
1169
|
+
// Skip if values are equal and move to next criterion
|
|
1170
|
+
if (aValue === bValue)
|
|
1171
|
+
continue;
|
|
1172
|
+
// Use the compareValues function for proper type handling
|
|
1173
|
+
return compareValues(aValue, bValue, direction);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return 0; // Equal if all criteria match
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
else if (hasCreatedAt) {
|
|
1180
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
1181
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1182
|
+
const aValue = a[createdAtField];
|
|
1183
|
+
const bValue = b[createdAtField];
|
|
1184
|
+
return compareValues(aValue, bValue, 'desc');
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
// Fetch the updated count after the data changes
|
|
1188
|
+
// For updates, the count might not change but we fetch anyway to be consistent
|
|
1189
|
+
setTimeout(() => fetchTotalCount(), 0);
|
|
1190
|
+
return sortedData;
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
else if (payload.eventType === 'DELETE') {
|
|
1194
|
+
// Process delete event
|
|
1195
|
+
console.log('🗑️ Processing DELETE event for', tableName);
|
|
1196
|
+
setData((prev) => {
|
|
1197
|
+
console.log('🗑️ DELETE: Current data before deletion:', prev.length, 'items');
|
|
1198
|
+
// Access current options via refs
|
|
1199
|
+
const currentWhere = whereRef.current;
|
|
1200
|
+
const currentOrderBy = orderByRef.current;
|
|
1201
|
+
const currentLimit = limitRef.current;
|
|
1202
|
+
const currentOffset = offsetRef.current;
|
|
1203
|
+
// Skip if search is active
|
|
1204
|
+
if (isSearchingRef.current) {
|
|
1205
|
+
console.log('⏭️ DELETE: Skipping - search is active');
|
|
1206
|
+
return prev;
|
|
1207
|
+
}
|
|
1208
|
+
// Save the current size before filtering
|
|
1209
|
+
const currentSize = prev.length;
|
|
1210
|
+
// Filter out the deleted item
|
|
1211
|
+
const filteredData = prev.filter((item) => {
|
|
1212
|
+
// @ts-ignore: Supabase typing issue
|
|
1213
|
+
const shouldKeep = !('id' in item && 'id' in payload.old && item.id === payload.old.id);
|
|
1214
|
+
if (!shouldKeep) {
|
|
1215
|
+
console.log('🗑️ DELETE: Removing item with ID:', item.id);
|
|
1216
|
+
}
|
|
1217
|
+
return shouldKeep;
|
|
1218
|
+
});
|
|
1219
|
+
console.log('🗑️ DELETE: Data after deletion:', filteredData.length, 'items (was', currentSize, ')');
|
|
1220
|
+
// Fetch the updated count after the data changes
|
|
1221
|
+
setTimeout(() => fetchTotalCount(), 0);
|
|
1222
|
+
// If we need to maintain the size with a limit
|
|
1223
|
+
if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) { // Use ref value
|
|
1224
|
+
console.log(`🗑️ DELETE: Record deleted with limit ${currentLimit}, will fetch additional record to maintain size`);
|
|
1225
|
+
// Use setTimeout to ensure this state update completes first
|
|
1226
|
+
setTimeout(() => {
|
|
1227
|
+
findMany({
|
|
1228
|
+
where: currentWhere, // Use ref value
|
|
1229
|
+
orderBy: currentOrderBy, // Use ref value
|
|
1230
|
+
take: currentLimit, // Use ref value
|
|
1231
|
+
skip: currentOffset // Use ref value (passed as skip to findMany)
|
|
1232
|
+
});
|
|
1233
|
+
}, 0);
|
|
1234
|
+
// Return the filtered data without resizing for now
|
|
1235
|
+
// The findMany call above will update the data later
|
|
1236
|
+
return filteredData;
|
|
1237
|
+
}
|
|
1238
|
+
// Re-apply ordering to maintain consistency
|
|
1239
|
+
let sortedData = [...filteredData];
|
|
1240
|
+
// Apply ordering if specified
|
|
1241
|
+
if (currentOrderBy) { // Use ref value
|
|
1242
|
+
// Convert orderBy to array format for consistency if it's an object
|
|
1243
|
+
const orderByArray = Array.isArray(currentOrderBy)
|
|
1244
|
+
? currentOrderBy
|
|
1245
|
+
: [currentOrderBy];
|
|
1246
|
+
// Apply each sort in sequence
|
|
1247
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1248
|
+
// Check each orderBy clause in sequence
|
|
1249
|
+
for (const orderByClause of orderByArray) {
|
|
1250
|
+
for (const [field, direction] of Object.entries(orderByClause)) {
|
|
1251
|
+
const aValue = a[field];
|
|
1252
|
+
const bValue = b[field];
|
|
1253
|
+
// Skip if values are equal and move to next criterion
|
|
1254
|
+
if (aValue === bValue)
|
|
1255
|
+
continue;
|
|
1256
|
+
// Use the compareValues function for proper type handling
|
|
1257
|
+
return compareValues(aValue, bValue, direction);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return 0; // Equal if all criteria match
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
else if (hasCreatedAt) {
|
|
1264
|
+
// Default sort by createdAt desc if no explicit sort but has timestamp
|
|
1265
|
+
sortedData = sortedData.sort((a, b) => {
|
|
1266
|
+
const aValue = a[createdAtField];
|
|
1267
|
+
const bValue = b[createdAtField];
|
|
1268
|
+
return compareValues(aValue, bValue, 'desc');
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
return sortedData;
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
.subscribe((status) => {
|
|
1276
|
+
console.log(`Subscription status for ${tableName}`, status);
|
|
1277
|
+
});
|
|
1278
|
+
// Store the channel ref (for optional introspection)
|
|
1279
|
+
channelRef.current = channel;
|
|
1280
|
+
return () => {
|
|
1281
|
+
console.log(`Unsubscribing from ${channelId}`);
|
|
1282
|
+
// Always remove the exact channel created by this effect instance
|
|
1283
|
+
supabase_client_1.supabase.removeChannel(channel);
|
|
1284
|
+
// Only clear the ref if it still matches (avoid races if effect re-runs)
|
|
1285
|
+
if (channelRef.current === channel) {
|
|
1286
|
+
channelRef.current = null;
|
|
1287
|
+
}
|
|
1288
|
+
if (searchTimeoutRef.current) {
|
|
1289
|
+
clearTimeout(searchTimeoutRef.current);
|
|
1290
|
+
searchTimeoutRef.current = null;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}, [realtime, channelName, tableName, enabled]); // NEVER include 'where' - subscription should persist
|
|
1294
|
+
// Create a memoized options object to prevent unnecessary re-renders
|
|
1295
|
+
const optionsRef = (0, react_1.useRef)({ where, orderBy, limit, offset, selectString });
|
|
1296
|
+
// Compare current options with previous options
|
|
1297
|
+
const optionsChanged = (0, react_1.useCallback)(() => {
|
|
1298
|
+
// Create stable string representations for deep comparison
|
|
1299
|
+
const whereStr = where ? JSON.stringify(where) : '';
|
|
1300
|
+
const orderByStr = orderBy ? JSON.stringify(orderBy) : '';
|
|
1301
|
+
const prevWhereStr = optionsRef.current.where ? JSON.stringify(optionsRef.current.where) : '';
|
|
1302
|
+
const prevOrderByStr = optionsRef.current.orderBy ? JSON.stringify(optionsRef.current.orderBy) : '';
|
|
1303
|
+
// Compare the stable representations
|
|
1304
|
+
const hasChanged = whereStr !== prevWhereStr ||
|
|
1305
|
+
orderByStr !== prevOrderByStr ||
|
|
1306
|
+
limit !== optionsRef.current.limit ||
|
|
1307
|
+
offset !== optionsRef.current.offset ||
|
|
1308
|
+
selectString !== optionsRef.current.selectString;
|
|
1309
|
+
if (hasChanged) {
|
|
1310
|
+
// Update the ref with the new values
|
|
1311
|
+
optionsRef.current = { where, orderBy, limit, offset, selectString };
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
}, [where, orderBy, limit, offset, selectString]);
|
|
1316
|
+
// Load initial data and refetch when options change (BUT NEVER TOUCH SUBSCRIPTION)
|
|
1317
|
+
(0, react_1.useEffect)(() => {
|
|
1318
|
+
// Skip fetching if not enabled
|
|
1319
|
+
if (!enabled) {
|
|
1320
|
+
setLoading(false); // Don't show loading spinner when disabled
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
// Skip if search is active
|
|
1324
|
+
if (isSearchingRef.current)
|
|
1325
|
+
return;
|
|
1326
|
+
// Skip if we've already loaded or if no filter criteria are provided
|
|
1327
|
+
if (initialLoadRef.current) {
|
|
1328
|
+
// Only reload if options have changed significantly
|
|
1329
|
+
if (optionsChanged()) {
|
|
1330
|
+
console.log(`Options changed for ${tableName}, refetching data (subscription stays alive)`);
|
|
1331
|
+
findMany({
|
|
1332
|
+
where,
|
|
1333
|
+
orderBy,
|
|
1334
|
+
take: limit,
|
|
1335
|
+
skip: offset
|
|
1336
|
+
});
|
|
1337
|
+
// Also update the total count
|
|
1338
|
+
fetchTotalCount();
|
|
1339
|
+
}
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
// Initial load
|
|
1343
|
+
initialLoadRef.current = true;
|
|
1344
|
+
findMany({
|
|
1345
|
+
where,
|
|
1346
|
+
orderBy,
|
|
1347
|
+
take: limit,
|
|
1348
|
+
skip: offset
|
|
1349
|
+
});
|
|
1350
|
+
// Initial count fetch
|
|
1351
|
+
fetchTotalCount();
|
|
1352
|
+
}, [findMany, where, orderBy, limit, offset, optionsChanged, fetchTotalCount, enabled]);
|
|
1353
|
+
// Track previous enabled state to detect changes from false to true
|
|
1354
|
+
const prevEnabledRef = (0, react_1.useRef)(enabled);
|
|
1355
|
+
// Fetch when enabled changes from false to true
|
|
1356
|
+
(0, react_1.useEffect)(() => {
|
|
1357
|
+
const wasDisabled = !prevEnabledRef.current;
|
|
1358
|
+
const isNowEnabled = enabled;
|
|
1359
|
+
// Update the previous value
|
|
1360
|
+
prevEnabledRef.current = enabled;
|
|
1361
|
+
// If we just became enabled and have already done initial load, refetch
|
|
1362
|
+
if (wasDisabled && isNowEnabled && initialLoadRef.current) {
|
|
1363
|
+
console.log(`Hook enabled for ${tableName}, fetching data`);
|
|
1364
|
+
findMany({
|
|
1365
|
+
where,
|
|
1366
|
+
orderBy,
|
|
1367
|
+
take: limit,
|
|
1368
|
+
skip: offset
|
|
1369
|
+
});
|
|
1370
|
+
fetchTotalCount();
|
|
1371
|
+
}
|
|
1372
|
+
}, [enabled, findMany, where, orderBy, limit, offset, fetchTotalCount]);
|
|
1373
|
+
/**
|
|
1374
|
+
* Create a new record with the provided data.
|
|
1375
|
+
* Default values from the schema will be applied if not provided.
|
|
1376
|
+
* NOTE: This operation does NOT immediately update the local state.
|
|
1377
|
+
* The state will be updated when the realtime INSERT event is received.
|
|
1378
|
+
*
|
|
1379
|
+
* @param data - The data to create the record with
|
|
1380
|
+
* @returns A promise with the created record or error
|
|
1381
|
+
*
|
|
1382
|
+
* @example
|
|
1383
|
+
* // Create a new user
|
|
1384
|
+
* const result = await users.create({
|
|
1385
|
+
* name: "John Doe",
|
|
1386
|
+
* email: "john@example.com"
|
|
1387
|
+
* });
|
|
1388
|
+
*
|
|
1389
|
+
* @example
|
|
1390
|
+
* // Create with custom ID (overriding default)
|
|
1391
|
+
* const result = await users.create({
|
|
1392
|
+
* id: "custom-id-123",
|
|
1393
|
+
* name: "John Doe"
|
|
1394
|
+
* });
|
|
1395
|
+
*/
|
|
1396
|
+
const create = (0, react_1.useCallback)(async (data) => {
|
|
1397
|
+
try {
|
|
1398
|
+
setLoading(true);
|
|
1399
|
+
setError(null);
|
|
1400
|
+
const now = new Date();
|
|
1401
|
+
// Helper function to convert Date objects to ISO strings for database
|
|
1402
|
+
const convertDatesForDatabase = (obj) => {
|
|
1403
|
+
const result = {};
|
|
1404
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1405
|
+
if (value instanceof Date) {
|
|
1406
|
+
result[key] = value.toISOString();
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
result[key] = value;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
};
|
|
1414
|
+
// Apply default values from schema
|
|
1415
|
+
const appliedDefaults = {};
|
|
1416
|
+
// Apply all default values that aren't already in the data
|
|
1417
|
+
for (const [field, defaultValue] of Object.entries(defaultValues)) {
|
|
1418
|
+
// @ts-ignore: Supabase typing issue
|
|
1419
|
+
if (!(field in data)) {
|
|
1420
|
+
// Parse the default value based on its type
|
|
1421
|
+
if (defaultValue.includes('now()') || defaultValue.includes('now')) {
|
|
1422
|
+
appliedDefaults[field] = now.toISOString(); // Database expects ISO string
|
|
1423
|
+
}
|
|
1424
|
+
else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
|
|
1425
|
+
appliedDefaults[field] = generateUUID();
|
|
1426
|
+
}
|
|
1427
|
+
else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
|
|
1428
|
+
// Simple cuid-like implementation for client-side
|
|
1429
|
+
appliedDefaults[field] = 'c' + Math.random().toString(36).substring(2, 15);
|
|
1430
|
+
}
|
|
1431
|
+
else if (defaultValue.includes('true')) {
|
|
1432
|
+
appliedDefaults[field] = true;
|
|
1433
|
+
}
|
|
1434
|
+
else if (defaultValue.includes('false')) {
|
|
1435
|
+
appliedDefaults[field] = false;
|
|
1436
|
+
}
|
|
1437
|
+
else if (!isNaN(Number(defaultValue))) {
|
|
1438
|
+
// If it's a number
|
|
1439
|
+
appliedDefaults[field] = Number(defaultValue);
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
// String or other value, remove quotes if present
|
|
1443
|
+
const strValue = defaultValue.replace(/^["'](.*)["']$/, '$1');
|
|
1444
|
+
appliedDefaults[field] = strValue;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const itemWithDefaults = convertDatesForDatabase({
|
|
1449
|
+
...appliedDefaults, // Apply schema defaults first
|
|
1450
|
+
...data, // Then user data (overrides defaults)
|
|
1451
|
+
// Use the actual field names from Prisma - convert Date to ISO string for database
|
|
1452
|
+
...(hasCreatedAt ? { [createdAtField]: now.toISOString() } : {}),
|
|
1453
|
+
...(hasUpdatedAt ? { [updatedAtField]: now.toISOString() } : {})
|
|
1454
|
+
});
|
|
1455
|
+
const { data: result, error } = await supabase_client_1.supabase
|
|
1456
|
+
.from(tableName)
|
|
1457
|
+
.insert([itemWithDefaults])
|
|
1458
|
+
.select(selectString);
|
|
1459
|
+
if (error)
|
|
1460
|
+
throw error;
|
|
1461
|
+
// DO NOT UPDATE LOCAL STATE HERE - Let realtime INSERT event handle it
|
|
1462
|
+
console.log('✅ Created ' + tableName + ' record, waiting for realtime INSERT event to update state');
|
|
1463
|
+
// Return created record
|
|
1464
|
+
return { data: result?.[0], error: null };
|
|
1465
|
+
}
|
|
1466
|
+
catch (err) {
|
|
1467
|
+
console.error('Error creating record:', err);
|
|
1468
|
+
setError(err);
|
|
1469
|
+
return { data: null, error: err };
|
|
1470
|
+
}
|
|
1471
|
+
finally {
|
|
1472
|
+
setLoading(false);
|
|
1473
|
+
}
|
|
1474
|
+
}, []);
|
|
1475
|
+
/**
|
|
1476
|
+
* Update an existing record identified by a unique identifier.
|
|
1477
|
+
* NOTE: This operation does NOT immediately update the local state.
|
|
1478
|
+
* The state will be updated when the realtime UPDATE event is received.
|
|
1479
|
+
*
|
|
1480
|
+
* @param params - Object containing the identifier and update data
|
|
1481
|
+
* @returns A promise with the updated record or error
|
|
1482
|
+
*
|
|
1483
|
+
* @example
|
|
1484
|
+
* // Update a user's name
|
|
1485
|
+
* const result = await users.update({
|
|
1486
|
+
* where: { id: "123" },
|
|
1487
|
+
* data: { name: "New Name" }
|
|
1488
|
+
* });
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* // Update multiple fields
|
|
1492
|
+
* const result = await users.update({
|
|
1493
|
+
* where: { id: "123" },
|
|
1494
|
+
* data: {
|
|
1495
|
+
* name: "New Name",
|
|
1496
|
+
* active: false
|
|
1497
|
+
* }
|
|
1498
|
+
* });
|
|
1499
|
+
*/
|
|
1500
|
+
const update = (0, react_1.useCallback)(async (params) => {
|
|
1501
|
+
try {
|
|
1502
|
+
setLoading(true);
|
|
1503
|
+
setError(null);
|
|
1504
|
+
// Find the primary field (usually 'id')
|
|
1505
|
+
// @ts-ignore: Supabase typing issue
|
|
1506
|
+
const primaryKey = Object.keys(params.where)[0];
|
|
1507
|
+
if (!primaryKey) {
|
|
1508
|
+
throw new Error('A unique identifier is required');
|
|
1509
|
+
}
|
|
1510
|
+
const value = params.where[primaryKey];
|
|
1511
|
+
if (value === undefined) {
|
|
1512
|
+
throw new Error('A unique identifier is required');
|
|
1513
|
+
}
|
|
1514
|
+
const now = new Date();
|
|
1515
|
+
// Helper function to convert Date objects to ISO strings for database
|
|
1516
|
+
const convertDatesForDatabase = (obj) => {
|
|
1517
|
+
const result = {};
|
|
1518
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1519
|
+
if (value instanceof Date) {
|
|
1520
|
+
result[key] = value.toISOString();
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
result[key] = value;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
1527
|
+
};
|
|
1528
|
+
// We do not apply default values for updates
|
|
1529
|
+
// Default values are only for creation, not for updates,
|
|
1530
|
+
// as existing records already have these values set
|
|
1531
|
+
const itemWithDefaults = convertDatesForDatabase({
|
|
1532
|
+
...params.data,
|
|
1533
|
+
// Use the actual updatedAt field name from Prisma - convert Date to ISO string for database
|
|
1534
|
+
...(hasUpdatedAt ? { [updatedAtField]: now.toISOString() } : {})
|
|
1535
|
+
});
|
|
1536
|
+
const { data, error } = await supabase_client_1.supabase
|
|
1537
|
+
.from(tableName)
|
|
1538
|
+
.update(itemWithDefaults)
|
|
1539
|
+
.eq(primaryKey, value)
|
|
1540
|
+
.select(selectString);
|
|
1541
|
+
if (error)
|
|
1542
|
+
throw error;
|
|
1543
|
+
// DO NOT UPDATE LOCAL STATE HERE - Let realtime UPDATE event handle it
|
|
1544
|
+
console.log('✅ Updated ' + tableName + ' record, waiting for realtime UPDATE event to update state');
|
|
1545
|
+
// Return updated record
|
|
1546
|
+
return { data: data?.[0], error: null };
|
|
1547
|
+
}
|
|
1548
|
+
catch (err) {
|
|
1549
|
+
console.error('Error updating record:', err);
|
|
1550
|
+
setError(err);
|
|
1551
|
+
return { data: null, error: err };
|
|
1552
|
+
}
|
|
1553
|
+
finally {
|
|
1554
|
+
setLoading(false);
|
|
1555
|
+
}
|
|
1556
|
+
}, []);
|
|
1557
|
+
/**
|
|
1558
|
+
* Delete a record by its unique identifier.
|
|
1559
|
+
* NOTE: This operation does NOT immediately update the local state.
|
|
1560
|
+
* The state will be updated when the realtime DELETE event is received.
|
|
1561
|
+
*
|
|
1562
|
+
* @param where - The unique identifier to delete the record by
|
|
1563
|
+
* @returns A promise with the deleted record or error
|
|
1564
|
+
*
|
|
1565
|
+
* @example
|
|
1566
|
+
* // Delete a user by ID
|
|
1567
|
+
* const result = await users.delete({ id: "123" });
|
|
1568
|
+
* if (result.data) {
|
|
1569
|
+
* console.log("Deleted user:", result.data.name);
|
|
1570
|
+
* }
|
|
1571
|
+
*/
|
|
1572
|
+
const deleteRecord = (0, react_1.useCallback)(async (where) => {
|
|
1573
|
+
try {
|
|
1574
|
+
setLoading(true);
|
|
1575
|
+
setError(null);
|
|
1576
|
+
// Find the primary field (usually 'id')
|
|
1577
|
+
// @ts-ignore: Supabase typing issue
|
|
1578
|
+
const primaryKey = Object.keys(where)[0];
|
|
1579
|
+
if (!primaryKey) {
|
|
1580
|
+
throw new Error('A unique identifier is required');
|
|
1581
|
+
}
|
|
1582
|
+
const value = where[primaryKey];
|
|
1583
|
+
if (value === undefined) {
|
|
1584
|
+
throw new Error('A unique identifier is required');
|
|
1585
|
+
}
|
|
1586
|
+
// First fetch the record to return it
|
|
1587
|
+
const { data: recordToDelete } = await supabase_client_1.supabase
|
|
1588
|
+
.from(tableName)
|
|
1589
|
+
.select(selectString)
|
|
1590
|
+
.eq(primaryKey, value)
|
|
1591
|
+
.maybeSingle();
|
|
1592
|
+
if (!recordToDelete) {
|
|
1593
|
+
throw new Error('Record not found');
|
|
1594
|
+
}
|
|
1595
|
+
// Then delete it
|
|
1596
|
+
const { error } = await supabase_client_1.supabase
|
|
1597
|
+
.from(tableName)
|
|
1598
|
+
.delete()
|
|
1599
|
+
.eq(primaryKey, value);
|
|
1600
|
+
if (error)
|
|
1601
|
+
throw error;
|
|
1602
|
+
// DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE event handle it
|
|
1603
|
+
console.log('✅ Deleted ' + tableName + ' record, waiting for realtime DELETE event to update state');
|
|
1604
|
+
// Return the deleted record
|
|
1605
|
+
return { data: recordToDelete, error: null };
|
|
1606
|
+
}
|
|
1607
|
+
catch (err) {
|
|
1608
|
+
console.error('Error deleting record:', err);
|
|
1609
|
+
setError(err);
|
|
1610
|
+
return { data: null, error: err };
|
|
1611
|
+
}
|
|
1612
|
+
finally {
|
|
1613
|
+
setLoading(false);
|
|
1614
|
+
}
|
|
1615
|
+
}, []);
|
|
1616
|
+
/**
|
|
1617
|
+
* Delete multiple records matching the filter criteria.
|
|
1618
|
+
* NOTE: This operation does NOT immediately update the local state.
|
|
1619
|
+
* The state will be updated when realtime DELETE events are received for each record.
|
|
1620
|
+
*
|
|
1621
|
+
* @param params - Query parameters for filtering records to delete
|
|
1622
|
+
* @returns A promise with the count of deleted records or error
|
|
1623
|
+
*
|
|
1624
|
+
* @example
|
|
1625
|
+
* // Delete all inactive users
|
|
1626
|
+
* const result = await users.deleteMany({
|
|
1627
|
+
* where: { active: false }
|
|
1628
|
+
* });
|
|
1629
|
+
* console.log('Deleted ' + result.count + ' inactive users');
|
|
1630
|
+
*
|
|
1631
|
+
* @example
|
|
1632
|
+
* // Delete all records (use with caution!)
|
|
1633
|
+
* const result = await users.deleteMany();
|
|
1634
|
+
*/
|
|
1635
|
+
const deleteMany = (0, react_1.useCallback)(async (params) => {
|
|
1636
|
+
try {
|
|
1637
|
+
setLoading(true);
|
|
1638
|
+
setError(null);
|
|
1639
|
+
// First, get the records that will be deleted to count them
|
|
1640
|
+
let query = supabase_client_1.supabase.from(tableName).select('*');
|
|
1641
|
+
// Apply where conditions if provided
|
|
1642
|
+
if (params?.where) {
|
|
1643
|
+
query = applyFilter(query, params.where);
|
|
1644
|
+
}
|
|
1645
|
+
// Get records that will be deleted
|
|
1646
|
+
const { data: recordsToDelete, error: fetchError } = await query;
|
|
1647
|
+
if (fetchError)
|
|
1648
|
+
throw fetchError;
|
|
1649
|
+
if (!recordsToDelete?.length) {
|
|
1650
|
+
return { count: 0, error: null };
|
|
1651
|
+
}
|
|
1652
|
+
// Build the delete query
|
|
1653
|
+
let deleteQuery = supabase_client_1.supabase.from(tableName).delete();
|
|
1654
|
+
// Apply the same filter to the delete operation
|
|
1655
|
+
if (params?.where) {
|
|
1656
|
+
// @ts-ignore: Supabase typing issue
|
|
1657
|
+
deleteQuery = applyFilter(deleteQuery, params.where);
|
|
1658
|
+
}
|
|
1659
|
+
// Perform the delete
|
|
1660
|
+
const { error: deleteError } = await deleteQuery;
|
|
1661
|
+
if (deleteError)
|
|
1662
|
+
throw deleteError;
|
|
1663
|
+
// DO NOT UPDATE LOCAL STATE HERE - Let realtime DELETE events handle it
|
|
1664
|
+
console.log('✅ Deleted ' + recordsToDelete.length + ' ' + tableName + ' records, waiting for realtime DELETE events to update state');
|
|
1665
|
+
// Return the count of deleted records
|
|
1666
|
+
return { count: recordsToDelete.length, error: null };
|
|
1667
|
+
}
|
|
1668
|
+
catch (err) {
|
|
1669
|
+
console.error('Error deleting multiple records:', err);
|
|
1670
|
+
setError(err);
|
|
1671
|
+
return { count: 0, error: err };
|
|
1672
|
+
}
|
|
1673
|
+
finally {
|
|
1674
|
+
setLoading(false);
|
|
1675
|
+
}
|
|
1676
|
+
}, []);
|
|
1677
|
+
/**
|
|
1678
|
+
* Find the first record matching the filter criteria.
|
|
1679
|
+
*
|
|
1680
|
+
* @param params - Query parameters for filtering and ordering
|
|
1681
|
+
* @returns A promise with the first matching record or error
|
|
1682
|
+
*
|
|
1683
|
+
* @example
|
|
1684
|
+
* // Find the first admin user
|
|
1685
|
+
* const result = await users.findFirst({
|
|
1686
|
+
* where: { role: 'admin' }
|
|
1687
|
+
* });
|
|
1688
|
+
*
|
|
1689
|
+
* @example
|
|
1690
|
+
* // Find the oldest post
|
|
1691
|
+
* const result = await posts.findFirst({
|
|
1692
|
+
* orderBy: { created_at: 'asc' }
|
|
1693
|
+
* });
|
|
1694
|
+
*/
|
|
1695
|
+
const findFirst = (0, react_1.useCallback)(async (params) => {
|
|
1696
|
+
try {
|
|
1697
|
+
const result = await findMany({
|
|
1698
|
+
...params,
|
|
1699
|
+
take: 1
|
|
1700
|
+
});
|
|
1701
|
+
if (result.error)
|
|
1702
|
+
return { data: null, error: result.error };
|
|
1703
|
+
if (!result.data.length)
|
|
1704
|
+
return { data: null, error: new Error('No records found') };
|
|
1705
|
+
// @ts-ignore: Supabase typing issue
|
|
1706
|
+
return { data: result.data[0], error: null };
|
|
1707
|
+
}
|
|
1708
|
+
catch (err) {
|
|
1709
|
+
console.error('Error finding first record:', err);
|
|
1710
|
+
return { data: null, error: err };
|
|
1711
|
+
}
|
|
1712
|
+
}, [findMany]);
|
|
1713
|
+
/**
|
|
1714
|
+
* Create a record if it doesn't exist, or update it if it does.
|
|
1715
|
+
*
|
|
1716
|
+
* @param params - Object containing the identifier, update data, and create data
|
|
1717
|
+
* @returns A promise with the created or updated record or error
|
|
1718
|
+
*
|
|
1719
|
+
* @example
|
|
1720
|
+
* // Upsert a user by ID
|
|
1721
|
+
* const result = await users.upsert({
|
|
1722
|
+
* where: { id: "123" },
|
|
1723
|
+
* update: { lastLogin: new Date().toISOString() },
|
|
1724
|
+
* create: {
|
|
1725
|
+
* id: "123",
|
|
1726
|
+
* name: "John Doe",
|
|
1727
|
+
* email: "john@example.com",
|
|
1728
|
+
* lastLogin: new Date().toISOString()
|
|
1729
|
+
* }
|
|
1730
|
+
* });
|
|
1731
|
+
*/
|
|
1732
|
+
const upsert = (0, react_1.useCallback)(async (params) => {
|
|
1733
|
+
try {
|
|
1734
|
+
// Check if record exists
|
|
1735
|
+
const { data: existing } = await findUnique(params.where);
|
|
1736
|
+
// Update if exists, otherwise create
|
|
1737
|
+
if (existing) {
|
|
1738
|
+
return update({ where: params.where, data: params.update });
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
return create(params.create);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
console.error('Error upserting record:', err);
|
|
1746
|
+
return { data: null, error: err };
|
|
1747
|
+
}
|
|
1748
|
+
}, [findUnique, update, create]);
|
|
1749
|
+
/**
|
|
1750
|
+
* Count the number of records matching the filter criteria.
|
|
1751
|
+
* This is a manual method to get the count with a different filter
|
|
1752
|
+
* than the main hook's filter.
|
|
1753
|
+
*
|
|
1754
|
+
* @param params - Query parameters for filtering
|
|
1755
|
+
* @returns A promise with the count of matching records
|
|
1756
|
+
*/
|
|
1757
|
+
const countFn = (0, react_1.useCallback)(async (params) => {
|
|
1758
|
+
try {
|
|
1759
|
+
let query = supabase_client_1.supabase.from(tableName).select('*', { count: 'exact', head: true });
|
|
1760
|
+
// Use provided where filter, or fall back to the hook's original where filter
|
|
1761
|
+
const effectiveWhere = params?.where ?? where;
|
|
1762
|
+
if (effectiveWhere) {
|
|
1763
|
+
query = applyFilter(query, effectiveWhere);
|
|
1764
|
+
}
|
|
1765
|
+
const { count, error } = await query;
|
|
1766
|
+
if (error)
|
|
1767
|
+
throw error;
|
|
1768
|
+
return count || 0;
|
|
1769
|
+
}
|
|
1770
|
+
catch (err) {
|
|
1771
|
+
console.error('Error counting records:', err);
|
|
1772
|
+
return 0;
|
|
1773
|
+
}
|
|
1774
|
+
}, [where]);
|
|
1775
|
+
/**
|
|
1776
|
+
* Manually refresh the data with current filter settings.
|
|
1777
|
+
* Useful after external operations or when realtime is disabled.
|
|
1778
|
+
*
|
|
1779
|
+
* @param params - Optional override parameters for this specific refresh
|
|
1780
|
+
* @returns A promise with the refreshed data or error
|
|
1781
|
+
*
|
|
1782
|
+
* @example
|
|
1783
|
+
* // Refresh with current filter settings
|
|
1784
|
+
* await users.refresh();
|
|
1785
|
+
*
|
|
1786
|
+
* @example
|
|
1787
|
+
* // Refresh with different filters for this call only
|
|
1788
|
+
* await users.refresh({
|
|
1789
|
+
* where: { active: true },
|
|
1790
|
+
* orderBy: { name: 'asc' }
|
|
1791
|
+
* });
|
|
1792
|
+
*/
|
|
1793
|
+
const refresh = (0, react_1.useCallback)((params) => {
|
|
1794
|
+
// If search is active, refresh search results
|
|
1795
|
+
if (isSearchingRef.current && searchQueries.length > 0) {
|
|
1796
|
+
executeSearch(searchQueries);
|
|
1797
|
+
return Promise.resolve({ data: data, error: null });
|
|
1798
|
+
}
|
|
1799
|
+
// Otherwise, refresh normal data using original params if not explicitly overridden
|
|
1800
|
+
return findMany({
|
|
1801
|
+
where: params?.where ?? where,
|
|
1802
|
+
orderBy: params?.orderBy ?? orderBy,
|
|
1803
|
+
take: params?.take ?? limit,
|
|
1804
|
+
skip: params?.skip ?? offset
|
|
1805
|
+
});
|
|
1806
|
+
}, [findMany, data, searchQueries, where, orderBy, limit, offset]);
|
|
1807
|
+
// Construct final hook API with or without search
|
|
1808
|
+
const api = {
|
|
1809
|
+
// State
|
|
1810
|
+
data,
|
|
1811
|
+
error,
|
|
1812
|
+
loading,
|
|
1813
|
+
count, // Now including count as a reactive state value
|
|
1814
|
+
// Finder methods
|
|
1815
|
+
findUnique,
|
|
1816
|
+
findMany,
|
|
1817
|
+
findFirst,
|
|
1818
|
+
// Mutation methods
|
|
1819
|
+
create,
|
|
1820
|
+
update,
|
|
1821
|
+
delete: deleteRecord,
|
|
1822
|
+
deleteMany,
|
|
1823
|
+
upsert,
|
|
1824
|
+
// Manual refresh
|
|
1825
|
+
refresh
|
|
1826
|
+
};
|
|
1827
|
+
// Add search object if searchable fields are present
|
|
1828
|
+
return searchFields.length > 0
|
|
1829
|
+
? {
|
|
1830
|
+
...api,
|
|
1831
|
+
search
|
|
1832
|
+
}
|
|
1833
|
+
: api;
|
|
1834
|
+
};
|
|
1835
|
+
}
|