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.
Files changed (132) hide show
  1. package/dist/generators/coreGenerator.js +97 -37
  2. package/dist/generators/hookGenerator.js +6 -2
  3. package/dist/generators/typeGenerator.js +43 -31
  4. package/dist/index.js +6 -1
  5. package/dist/suparisma/generated/utils/core.js +1835 -0
  6. package/package.json +1 -1
  7. package/tmp/generated-test/hooks/useSuparismaAsset.ts +94 -0
  8. package/tmp/generated-test/hooks/useSuparismaChapter.ts +96 -0
  9. package/tmp/generated-test/hooks/useSuparismaCourse.ts +96 -0
  10. package/tmp/generated-test/hooks/useSuparismaDeviceSession.ts +94 -0
  11. package/tmp/generated-test/hooks/useSuparismaEnrollment.ts +92 -0
  12. package/tmp/generated-test/hooks/useSuparismaLesson.ts +96 -0
  13. package/tmp/generated-test/hooks/useSuparismaLessonPurchase.ts +92 -0
  14. package/tmp/generated-test/hooks/useSuparismaLessonQuestion.ts +96 -0
  15. package/tmp/generated-test/hooks/useSuparismaPayoutMethod.ts +96 -0
  16. package/tmp/generated-test/hooks/useSuparismaPayoutRequest.ts +96 -0
  17. package/tmp/generated-test/hooks/useSuparismaQuestionOption.ts +92 -0
  18. package/tmp/generated-test/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
  19. package/tmp/generated-test/hooks/useSuparismaTeacherPayoutInfo.ts +96 -0
  20. package/tmp/generated-test/hooks/useSuparismaThing.ts +96 -0
  21. package/tmp/generated-test/hooks/useSuparismaUser.ts +96 -0
  22. package/tmp/generated-test/hooks/useSuparismaVideoNote.ts +96 -0
  23. package/tmp/generated-test/hooks/useSuparismaWallet.ts +96 -0
  24. package/tmp/generated-test/hooks/useSuparismaWalletTransaction.ts +96 -0
  25. package/tmp/generated-test/hooks/useSuparismaWatchProgress.ts +96 -0
  26. package/tmp/generated-test/index.ts +140 -0
  27. package/tmp/generated-test/types/AssetTypes.ts +485 -0
  28. package/tmp/generated-test/types/ChapterTypes.ts +488 -0
  29. package/tmp/generated-test/types/CourseTypes.ts +519 -0
  30. package/tmp/generated-test/types/DeviceSessionTypes.ts +489 -0
  31. package/tmp/generated-test/types/EnrollmentTypes.ts +495 -0
  32. package/tmp/generated-test/types/LessonPurchaseTypes.ts +490 -0
  33. package/tmp/generated-test/types/LessonQuestionTypes.ts +496 -0
  34. package/tmp/generated-test/types/LessonTypes.ts +517 -0
  35. package/tmp/generated-test/types/PayoutMethodTypes.ts +517 -0
  36. package/tmp/generated-test/types/PayoutRequestTypes.ts +528 -0
  37. package/tmp/generated-test/types/QuestionOptionTypes.ts +479 -0
  38. package/tmp/generated-test/types/SavedPaymentMethodTypes.ts +497 -0
  39. package/tmp/generated-test/types/TeacherPayoutInfoTypes.ts +480 -0
  40. package/tmp/generated-test/types/ThingTypes.ts +482 -0
  41. package/tmp/generated-test/types/UserTypes.ts +487 -0
  42. package/tmp/generated-test/types/VideoNoteTypes.ts +489 -0
  43. package/tmp/generated-test/types/WalletTransactionTypes.ts +505 -0
  44. package/tmp/generated-test/types/WalletTypes.ts +480 -0
  45. package/tmp/generated-test/types/WatchProgressTypes.ts +493 -0
  46. package/tmp/generated-test/utils/core.ts +2306 -0
  47. package/tmp/generated-test/utils/supabase-client.ts +17 -0
  48. package/tmp/generated-test2/hooks/useSuparismaAsset.ts +94 -0
  49. package/tmp/generated-test2/hooks/useSuparismaChapter.ts +96 -0
  50. package/tmp/generated-test2/hooks/useSuparismaCourse.ts +96 -0
  51. package/tmp/generated-test2/hooks/useSuparismaDeviceSession.ts +94 -0
  52. package/tmp/generated-test2/hooks/useSuparismaEnrollment.ts +92 -0
  53. package/tmp/generated-test2/hooks/useSuparismaLesson.ts +96 -0
  54. package/tmp/generated-test2/hooks/useSuparismaLessonPurchase.ts +92 -0
  55. package/tmp/generated-test2/hooks/useSuparismaLessonQuestion.ts +96 -0
  56. package/tmp/generated-test2/hooks/useSuparismaPayoutMethod.ts +96 -0
  57. package/tmp/generated-test2/hooks/useSuparismaPayoutRequest.ts +96 -0
  58. package/tmp/generated-test2/hooks/useSuparismaQuestionOption.ts +92 -0
  59. package/tmp/generated-test2/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
  60. package/tmp/generated-test2/hooks/useSuparismaTeacherPayoutInfo.ts +96 -0
  61. package/tmp/generated-test2/hooks/useSuparismaThing.ts +96 -0
  62. package/tmp/generated-test2/hooks/useSuparismaUser.ts +96 -0
  63. package/tmp/generated-test2/hooks/useSuparismaVideoNote.ts +96 -0
  64. package/tmp/generated-test2/hooks/useSuparismaWallet.ts +96 -0
  65. package/tmp/generated-test2/hooks/useSuparismaWalletTransaction.ts +96 -0
  66. package/tmp/generated-test2/hooks/useSuparismaWatchProgress.ts +96 -0
  67. package/tmp/generated-test2/index.ts +140 -0
  68. package/tmp/generated-test2/types/AssetTypes.ts +485 -0
  69. package/tmp/generated-test2/types/ChapterTypes.ts +488 -0
  70. package/tmp/generated-test2/types/CourseTypes.ts +522 -0
  71. package/tmp/generated-test2/types/DeviceSessionTypes.ts +489 -0
  72. package/tmp/generated-test2/types/EnrollmentTypes.ts +495 -0
  73. package/tmp/generated-test2/types/LessonPurchaseTypes.ts +490 -0
  74. package/tmp/generated-test2/types/LessonQuestionTypes.ts +496 -0
  75. package/tmp/generated-test2/types/LessonTypes.ts +517 -0
  76. package/tmp/generated-test2/types/PayoutMethodTypes.ts +517 -0
  77. package/tmp/generated-test2/types/PayoutRequestTypes.ts +528 -0
  78. package/tmp/generated-test2/types/QuestionOptionTypes.ts +479 -0
  79. package/tmp/generated-test2/types/SavedPaymentMethodTypes.ts +497 -0
  80. package/tmp/generated-test2/types/TeacherPayoutInfoTypes.ts +480 -0
  81. package/tmp/generated-test2/types/ThingTypes.ts +482 -0
  82. package/tmp/generated-test2/types/UserTypes.ts +490 -0
  83. package/tmp/generated-test2/types/VideoNoteTypes.ts +489 -0
  84. package/tmp/generated-test2/types/WalletTransactionTypes.ts +505 -0
  85. package/tmp/generated-test2/types/WalletTypes.ts +480 -0
  86. package/tmp/generated-test2/types/WatchProgressTypes.ts +493 -0
  87. package/tmp/generated-test2/utils/core.ts +2306 -0
  88. package/tmp/generated-test2/utils/supabase-client.ts +17 -0
  89. package/tmp/generated-test3/hooks/useSuparismaAsset.ts +94 -0
  90. package/tmp/generated-test3/hooks/useSuparismaChapter.ts +98 -0
  91. package/tmp/generated-test3/hooks/useSuparismaCourse.ts +98 -0
  92. package/tmp/generated-test3/hooks/useSuparismaDeviceSession.ts +94 -0
  93. package/tmp/generated-test3/hooks/useSuparismaEnrollment.ts +94 -0
  94. package/tmp/generated-test3/hooks/useSuparismaLesson.ts +98 -0
  95. package/tmp/generated-test3/hooks/useSuparismaLessonPurchase.ts +94 -0
  96. package/tmp/generated-test3/hooks/useSuparismaLessonQuestion.ts +98 -0
  97. package/tmp/generated-test3/hooks/useSuparismaPayoutMethod.ts +96 -0
  98. package/tmp/generated-test3/hooks/useSuparismaPayoutRequest.ts +96 -0
  99. package/tmp/generated-test3/hooks/useSuparismaQuestionOption.ts +94 -0
  100. package/tmp/generated-test3/hooks/useSuparismaSavedPaymentMethod.ts +96 -0
  101. package/tmp/generated-test3/hooks/useSuparismaTeacherPayoutInfo.ts +98 -0
  102. package/tmp/generated-test3/hooks/useSuparismaThing.ts +96 -0
  103. package/tmp/generated-test3/hooks/useSuparismaUser.ts +98 -0
  104. package/tmp/generated-test3/hooks/useSuparismaVideoNote.ts +98 -0
  105. package/tmp/generated-test3/hooks/useSuparismaWallet.ts +98 -0
  106. package/tmp/generated-test3/hooks/useSuparismaWalletTransaction.ts +98 -0
  107. package/tmp/generated-test3/hooks/useSuparismaWatchProgress.ts +98 -0
  108. package/tmp/generated-test3/index.ts +140 -0
  109. package/tmp/generated-test3/types/AssetTypes.ts +485 -0
  110. package/tmp/generated-test3/types/ChapterTypes.ts +488 -0
  111. package/tmp/generated-test3/types/CourseTypes.ts +522 -0
  112. package/tmp/generated-test3/types/DeviceSessionTypes.ts +489 -0
  113. package/tmp/generated-test3/types/EnrollmentTypes.ts +495 -0
  114. package/tmp/generated-test3/types/LessonPurchaseTypes.ts +490 -0
  115. package/tmp/generated-test3/types/LessonQuestionTypes.ts +496 -0
  116. package/tmp/generated-test3/types/LessonTypes.ts +517 -0
  117. package/tmp/generated-test3/types/PayoutMethodTypes.ts +517 -0
  118. package/tmp/generated-test3/types/PayoutRequestTypes.ts +528 -0
  119. package/tmp/generated-test3/types/QuestionOptionTypes.ts +479 -0
  120. package/tmp/generated-test3/types/SavedPaymentMethodTypes.ts +497 -0
  121. package/tmp/generated-test3/types/TeacherPayoutInfoTypes.ts +480 -0
  122. package/tmp/generated-test3/types/ThingTypes.ts +482 -0
  123. package/tmp/generated-test3/types/UserTypes.ts +490 -0
  124. package/tmp/generated-test3/types/VideoNoteTypes.ts +489 -0
  125. package/tmp/generated-test3/types/WalletTransactionTypes.ts +505 -0
  126. package/tmp/generated-test3/types/WalletTypes.ts +480 -0
  127. package/tmp/generated-test3/types/WatchProgressTypes.ts +493 -0
  128. package/tmp/generated-test3/utils/core.ts +2316 -0
  129. package/tmp/generated-test3/utils/supabase-client.ts +17 -0
  130. package/tmp/prisma-test-schema-2.prisma +339 -0
  131. package/tmp/prisma-test-schema.prisma +317 -0
  132. 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
+ }