suparisma 0.0.3 → 1.0.0
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/README.md +708 -72
- package/dist/generators/coreGenerator.js +1 -1
- package/dist/index.js +0 -0
- package/generated/hooks/useSuparismaAuditLog.ts +80 -0
- package/generated/hooks/useSuparismaThing.ts +82 -0
- package/generated/index.ts +55 -0
- package/generated/types/AuditLogTypes.ts +391 -0
- package/generated/types/ThingTypes.ts +394 -0
- package/generated/utils/core.ts +1490 -0
- package/generated/utils/supabase-client.ts +10 -0
- package/package.json +8 -1
- package/dist/generated/supabase-client-generated.js +0 -7
- package/dist/hooks/generated/UserTypes.js +0 -2
- package/dist/hooks/generated/core.js +0 -1089
- package/dist/hooks/generated/index.js +0 -33
- package/dist/hooks/generated/useSuparismaUser.js +0 -60
- package/dist/suparisma/generated/hooks/useSuparismaUser.js +0 -61
- package/dist/suparisma/generated/index.js +0 -33
- package/dist/suparisma/generated/types/UserTypes.js +0 -4
- package/dist/suparisma/generated/utils/core.js +0 -1090
- package/dist/suparisma/generated/utils/supabase-client.js +0 -8
|
@@ -1,1090 +0,0 @@
|
|
|
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.buildFilterString = buildFilterString;
|
|
6
|
-
exports.applyFilter = applyFilter;
|
|
7
|
-
exports.applyOrderBy = applyOrderBy;
|
|
8
|
-
exports.createSuparismaHook = createSuparismaHook;
|
|
9
|
-
const react_1 = require("react");
|
|
10
|
-
// This import should be relative to its new location in utils/
|
|
11
|
-
const supabase_client_1 = require("./supabase-client");
|
|
12
|
-
/**
|
|
13
|
-
* Convert a type-safe where filter to Supabase filter string
|
|
14
|
-
*/
|
|
15
|
-
function buildFilterString(where) {
|
|
16
|
-
if (!where)
|
|
17
|
-
return undefined;
|
|
18
|
-
const filters = [];
|
|
19
|
-
for (const [key, value] of Object.entries(where)) {
|
|
20
|
-
if (value !== undefined) {
|
|
21
|
-
if (typeof value === 'object' && value !== null) {
|
|
22
|
-
// Handle advanced operators
|
|
23
|
-
const advancedOps = value;
|
|
24
|
-
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
25
|
-
filters.push(`${key}=eq.${advancedOps.equals}`);
|
|
26
|
-
}
|
|
27
|
-
if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
28
|
-
filters.push(`${key}=neq.${advancedOps.not}`);
|
|
29
|
-
}
|
|
30
|
-
if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
31
|
-
filters.push(`${key}=gt.${advancedOps.gt}`);
|
|
32
|
-
}
|
|
33
|
-
if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
34
|
-
filters.push(`${key}=gte.${advancedOps.gte}`);
|
|
35
|
-
}
|
|
36
|
-
if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
37
|
-
filters.push(`${key}=lt.${advancedOps.lt}`);
|
|
38
|
-
}
|
|
39
|
-
if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
40
|
-
filters.push(`${key}=lte.${advancedOps.lte}`);
|
|
41
|
-
}
|
|
42
|
-
if ('in' in advancedOps && advancedOps.in?.length) {
|
|
43
|
-
filters.push(`${key}=in.(${advancedOps.in.join(',')})`);
|
|
44
|
-
}
|
|
45
|
-
if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
46
|
-
filters.push(`${key}=ilike.*${advancedOps.contains}*`);
|
|
47
|
-
}
|
|
48
|
-
if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
49
|
-
filters.push(`${key}=ilike.${advancedOps.startsWith}%`);
|
|
50
|
-
}
|
|
51
|
-
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
52
|
-
filters.push(`${key}=ilike.%${advancedOps.endsWith}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
// Simple equality
|
|
57
|
-
filters.push(`${key}=eq.${value}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return filters.length > 0 ? filters.join(',') : undefined;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Apply filter to the query builder
|
|
65
|
-
*/
|
|
66
|
-
function applyFilter(query, where) {
|
|
67
|
-
if (!where)
|
|
68
|
-
return query;
|
|
69
|
-
let filteredQuery = query;
|
|
70
|
-
// Apply each filter condition
|
|
71
|
-
for (const [key, value] of Object.entries(where)) {
|
|
72
|
-
if (value !== undefined) {
|
|
73
|
-
if (typeof value === 'object' && value !== null) {
|
|
74
|
-
// Handle advanced operators
|
|
75
|
-
const advancedOps = value;
|
|
76
|
-
if ('equals' in advancedOps && advancedOps.equals !== undefined) {
|
|
77
|
-
// @ts-ignore: Supabase typing issue
|
|
78
|
-
filteredQuery = filteredQuery.eq(key, advancedOps.equals);
|
|
79
|
-
}
|
|
80
|
-
if ('not' in advancedOps && advancedOps.not !== undefined) {
|
|
81
|
-
// @ts-ignore: Supabase typing issue
|
|
82
|
-
filteredQuery = filteredQuery.neq(key, advancedOps.not);
|
|
83
|
-
}
|
|
84
|
-
if ('gt' in advancedOps && advancedOps.gt !== undefined) {
|
|
85
|
-
// @ts-ignore: Supabase typing issue
|
|
86
|
-
filteredQuery = filteredQuery.gt(key, advancedOps.gt);
|
|
87
|
-
}
|
|
88
|
-
if ('gte' in advancedOps && advancedOps.gte !== undefined) {
|
|
89
|
-
// @ts-ignore: Supabase typing issue
|
|
90
|
-
filteredQuery = filteredQuery.gte(key, advancedOps.gte);
|
|
91
|
-
}
|
|
92
|
-
if ('lt' in advancedOps && advancedOps.lt !== undefined) {
|
|
93
|
-
// @ts-ignore: Supabase typing issue
|
|
94
|
-
filteredQuery = filteredQuery.lt(key, advancedOps.lt);
|
|
95
|
-
}
|
|
96
|
-
if ('lte' in advancedOps && advancedOps.lte !== undefined) {
|
|
97
|
-
// @ts-ignore: Supabase typing issue
|
|
98
|
-
filteredQuery = filteredQuery.lte(key, advancedOps.lte);
|
|
99
|
-
}
|
|
100
|
-
if ('in' in advancedOps && advancedOps.in?.length) {
|
|
101
|
-
// @ts-ignore: Supabase typing issue
|
|
102
|
-
filteredQuery = filteredQuery.in(key, advancedOps.in);
|
|
103
|
-
}
|
|
104
|
-
if ('contains' in advancedOps && advancedOps.contains !== undefined) {
|
|
105
|
-
// @ts-ignore: Supabase typing issue
|
|
106
|
-
filteredQuery = filteredQuery.ilike(key, `*${advancedOps.contains}*`);
|
|
107
|
-
}
|
|
108
|
-
if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
|
|
109
|
-
// @ts-ignore: Supabase typing issue
|
|
110
|
-
filteredQuery = filteredQuery.ilike(key, `${advancedOps.startsWith}%`);
|
|
111
|
-
}
|
|
112
|
-
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
113
|
-
// @ts-ignore: Supabase typing issue
|
|
114
|
-
filteredQuery = filteredQuery.ilike(key, `%${advancedOps.endsWith}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
// Simple equality
|
|
119
|
-
// @ts-ignore: Supabase typing issue
|
|
120
|
-
filteredQuery = filteredQuery.eq(key, value);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return filteredQuery;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Apply order by to the query builder
|
|
128
|
-
*/
|
|
129
|
-
function applyOrderBy(query, orderBy, hasCreatedAt) {
|
|
130
|
-
if (!orderBy) {
|
|
131
|
-
// By default, sort by created_at if available
|
|
132
|
-
if (hasCreatedAt) {
|
|
133
|
-
// @ts-ignore: Supabase typing issue
|
|
134
|
-
return query.order('created_at', { ascending: false });
|
|
135
|
-
}
|
|
136
|
-
return query;
|
|
137
|
-
}
|
|
138
|
-
// Apply each order by clause
|
|
139
|
-
let orderedQuery = query;
|
|
140
|
-
for (const [key, direction] of Object.entries(orderBy)) {
|
|
141
|
-
// @ts-ignore: Supabase typing issue
|
|
142
|
-
orderedQuery = orderedQuery.order(key, {
|
|
143
|
-
ascending: direction === 'asc'
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
return orderedQuery;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Core hook factory function that creates a type-safe realtime hook for a specific model.
|
|
150
|
-
* This is the foundation for all Suparisma hooks.
|
|
151
|
-
*/
|
|
152
|
-
function createSuparismaHook(config) {
|
|
153
|
-
const { tableName, hasCreatedAt, hasUpdatedAt, searchFields = [], defaultValues = {} } = config;
|
|
154
|
-
/**
|
|
155
|
-
* The main hook function that provides all data access methods for a model.
|
|
156
|
-
*
|
|
157
|
-
* @param options - Optional configuration for data fetching, filtering, and realtime
|
|
158
|
-
*
|
|
159
|
-
* @returns An API object with data state and CRUD methods
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* // Basic usage
|
|
163
|
-
* const users = useSuparismaUser();
|
|
164
|
-
* const { data, loading, error } = users;
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* // With filtering
|
|
168
|
-
* const users = useSuparismaUser({
|
|
169
|
-
* where: { role: 'admin' },
|
|
170
|
-
* orderBy: { created_at: 'desc' }
|
|
171
|
-
* });
|
|
172
|
-
*/
|
|
173
|
-
return function useSuparismaHook(options = {}) {
|
|
174
|
-
const { realtime = true, channelName, where, realtimeFilter, orderBy, limit, offset, } = options;
|
|
175
|
-
// Compute the actual filter string from the type-safe where object or use legacy filter
|
|
176
|
-
const computedFilter = where ? buildFilterString(where) : realtimeFilter;
|
|
177
|
-
// Single data collection for holding results
|
|
178
|
-
const [data, setData] = (0, react_1.useState)([]);
|
|
179
|
-
const [error, setError] = (0, react_1.useState)(null);
|
|
180
|
-
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
181
|
-
// Search state
|
|
182
|
-
const [searchQueries, setSearchQueries] = (0, react_1.useState)([]);
|
|
183
|
-
const [searchLoading, setSearchLoading] = (0, react_1.useState)(false);
|
|
184
|
-
const initialLoadRef = (0, react_1.useRef)(false);
|
|
185
|
-
const channelRef = (0, react_1.useRef)(null);
|
|
186
|
-
const searchTimeoutRef = (0, react_1.useRef)(null);
|
|
187
|
-
const isSearchingRef = (0, react_1.useRef)(false);
|
|
188
|
-
// Create the search state object with all required methods
|
|
189
|
-
const search = {
|
|
190
|
-
queries: searchQueries,
|
|
191
|
-
loading: searchLoading,
|
|
192
|
-
// Set all search queries at once
|
|
193
|
-
setQueries: (0, react_1.useCallback)((queries) => {
|
|
194
|
-
// Validate that all fields are searchable
|
|
195
|
-
const validQueries = queries.filter(query => searchFields.includes(query.field) && query.value.trim() !== '');
|
|
196
|
-
setSearchQueries(validQueries);
|
|
197
|
-
// Execute search if there are valid queries
|
|
198
|
-
if (validQueries.length > 0) {
|
|
199
|
-
executeSearch(validQueries);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
// If no valid queries, reset to normal data fetching
|
|
203
|
-
isSearchingRef.current = false;
|
|
204
|
-
findMany({ where, orderBy, take: limit, skip: offset });
|
|
205
|
-
}
|
|
206
|
-
}, [where, orderBy, limit, offset]),
|
|
207
|
-
// Add a single search query
|
|
208
|
-
addQuery: (0, react_1.useCallback)((query) => {
|
|
209
|
-
// Validate that the field is searchable
|
|
210
|
-
if (!searchFields.includes(query.field) || query.value.trim() === '') {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
setSearchQueries(prev => {
|
|
214
|
-
// Replace if query for this field already exists, otherwise add
|
|
215
|
-
const exists = prev.some(q => q.field === query.field);
|
|
216
|
-
const newQueries = exists
|
|
217
|
-
? prev.map(q => q.field === query.field ? query : q)
|
|
218
|
-
: [...prev, query];
|
|
219
|
-
// Execute search with updated queries
|
|
220
|
-
executeSearch(newQueries);
|
|
221
|
-
return newQueries;
|
|
222
|
-
});
|
|
223
|
-
}, []),
|
|
224
|
-
// Remove a search query by field
|
|
225
|
-
removeQuery: (0, react_1.useCallback)((field) => {
|
|
226
|
-
setSearchQueries(prev => {
|
|
227
|
-
const newQueries = prev.filter(q => q.field !== field);
|
|
228
|
-
// If we still have queries, execute search with remaining queries
|
|
229
|
-
if (newQueries.length > 0) {
|
|
230
|
-
executeSearch(newQueries);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
// If no queries left, reset to normal data fetching
|
|
234
|
-
isSearchingRef.current = false;
|
|
235
|
-
findMany({ where, orderBy, take: limit, skip: offset });
|
|
236
|
-
}
|
|
237
|
-
return newQueries;
|
|
238
|
-
});
|
|
239
|
-
}, [where, orderBy, limit, offset]),
|
|
240
|
-
// Clear all search queries
|
|
241
|
-
clearQueries: (0, react_1.useCallback)(() => {
|
|
242
|
-
setSearchQueries([]);
|
|
243
|
-
isSearchingRef.current = false;
|
|
244
|
-
findMany({ where, orderBy, take: limit, skip: offset });
|
|
245
|
-
}, [where, orderBy, limit, offset])
|
|
246
|
-
};
|
|
247
|
-
// Execute search based on queries
|
|
248
|
-
const executeSearch = (0, react_1.useCallback)(async (queries) => {
|
|
249
|
-
// Clear the previous timeout
|
|
250
|
-
if (searchTimeoutRef.current) {
|
|
251
|
-
clearTimeout(searchTimeoutRef.current);
|
|
252
|
-
}
|
|
253
|
-
// Skip if no searchable fields or no valid queries
|
|
254
|
-
if (searchFields.length === 0 || queries.length === 0) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
setSearchLoading(true);
|
|
258
|
-
isSearchingRef.current = true;
|
|
259
|
-
// Use debounce to prevent rapid searches
|
|
260
|
-
searchTimeoutRef.current = setTimeout(async () => {
|
|
261
|
-
try {
|
|
262
|
-
let results = [];
|
|
263
|
-
// Execute RPC function for each query using Promise.all
|
|
264
|
-
const searchPromises = queries.map(query => {
|
|
265
|
-
// Build function name: search_tablename_by_fieldname_prefix
|
|
266
|
-
const functionName = `search_${tableName}_by_${query.field}_prefix`;
|
|
267
|
-
// Call RPC function
|
|
268
|
-
return supabase_client_1.supabase.rpc(functionName, { prefix: query.value.trim() });
|
|
269
|
-
});
|
|
270
|
-
// Execute all search queries in parallel
|
|
271
|
-
const searchResults = await Promise.all(searchPromises);
|
|
272
|
-
// Combine and deduplicate results
|
|
273
|
-
const allResults = {};
|
|
274
|
-
// Process each search result
|
|
275
|
-
searchResults.forEach((result, index) => {
|
|
276
|
-
if (result.error) {
|
|
277
|
-
console.error(`Search error for ${queries[index]?.field}:`, result.error);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (result.data) {
|
|
281
|
-
// Add each result, using id as key to deduplicate
|
|
282
|
-
for (const item of result.data) {
|
|
283
|
-
// @ts-ignore: Assume item has an id property
|
|
284
|
-
if (item.id) {
|
|
285
|
-
// @ts-ignore: Add to results using id as key
|
|
286
|
-
allResults[item.id] = item;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
// Convert back to array
|
|
292
|
-
results = Object.values(allResults);
|
|
293
|
-
// Apply any where conditions client-side
|
|
294
|
-
if (where) {
|
|
295
|
-
results = results.filter((item) => {
|
|
296
|
-
for (const [key, value] of Object.entries(where)) {
|
|
297
|
-
if (typeof value === 'object' && value !== null) {
|
|
298
|
-
// Skip complex filters for now
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (item[key] !== value) {
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return true;
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
// Apply ordering if needed
|
|
309
|
-
if (orderBy) {
|
|
310
|
-
const orderEntries = Object.entries(orderBy);
|
|
311
|
-
if (orderEntries.length > 0) {
|
|
312
|
-
const [orderField, direction] = orderEntries[0] || [];
|
|
313
|
-
results = [...results].sort((a, b) => {
|
|
314
|
-
const aValue = a[orderField] ?? '';
|
|
315
|
-
const bValue = b[orderField] ?? '';
|
|
316
|
-
if (direction === 'asc') {
|
|
317
|
-
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Apply pagination if needed
|
|
326
|
-
if (limit && limit > 0) {
|
|
327
|
-
results = results.slice(0, limit);
|
|
328
|
-
}
|
|
329
|
-
if (offset && offset > 0) {
|
|
330
|
-
results = results.slice(offset);
|
|
331
|
-
}
|
|
332
|
-
// Update data with search results
|
|
333
|
-
setData(results);
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
console.error('Search error:', err);
|
|
337
|
-
setError(err);
|
|
338
|
-
}
|
|
339
|
-
finally {
|
|
340
|
-
setSearchLoading(false);
|
|
341
|
-
}
|
|
342
|
-
}, 300); // 300ms debounce
|
|
343
|
-
}, [tableName, searchFields, where, orderBy, limit, offset]);
|
|
344
|
-
/**
|
|
345
|
-
* Fetch multiple records with support for filtering, sorting, and pagination.
|
|
346
|
-
*
|
|
347
|
-
* @param params - Query parameters for filtering and ordering records
|
|
348
|
-
* @returns A promise with the fetched data or error
|
|
349
|
-
*
|
|
350
|
-
* @example
|
|
351
|
-
* // Get all active users
|
|
352
|
-
* const result = await users.findMany({
|
|
353
|
-
* where: { active: true }
|
|
354
|
-
* });
|
|
355
|
-
*
|
|
356
|
-
* @example
|
|
357
|
-
* // Get 10 most recent posts with pagination
|
|
358
|
-
* const page1 = await posts.findMany({
|
|
359
|
-
* orderBy: { created_at: 'desc' },
|
|
360
|
-
* take: 10,
|
|
361
|
-
* skip: 0
|
|
362
|
-
* });
|
|
363
|
-
*
|
|
364
|
-
* const page2 = await posts.findMany({
|
|
365
|
-
* orderBy: { created_at: 'desc' },
|
|
366
|
-
* take: 10,
|
|
367
|
-
* skip: 10
|
|
368
|
-
* });
|
|
369
|
-
*/
|
|
370
|
-
const findMany = (0, react_1.useCallback)(async (params) => {
|
|
371
|
-
try {
|
|
372
|
-
setLoading(true);
|
|
373
|
-
setError(null);
|
|
374
|
-
let query = supabase_client_1.supabase.from(tableName).select('*');
|
|
375
|
-
// Apply where conditions if provided
|
|
376
|
-
if (params?.where) {
|
|
377
|
-
query = applyFilter(query, params.where);
|
|
378
|
-
}
|
|
379
|
-
// Apply order by if provided
|
|
380
|
-
if (params?.orderBy) {
|
|
381
|
-
query = applyOrderBy(query, params.orderBy, hasCreatedAt);
|
|
382
|
-
}
|
|
383
|
-
else if (hasCreatedAt) {
|
|
384
|
-
// Default ordering if available
|
|
385
|
-
// @ts-ignore: Supabase typing issue
|
|
386
|
-
query = query.order('created_at', { ascending: false });
|
|
387
|
-
}
|
|
388
|
-
// Apply limit if provided
|
|
389
|
-
if (params?.take) {
|
|
390
|
-
query = query.limit(params.take);
|
|
391
|
-
}
|
|
392
|
-
// Apply offset if provided (for pagination)
|
|
393
|
-
if (params?.skip) {
|
|
394
|
-
query = query.range(params.skip, params.skip + (params.take || 10) - 1);
|
|
395
|
-
}
|
|
396
|
-
const { data, error } = await query;
|
|
397
|
-
if (error)
|
|
398
|
-
throw error;
|
|
399
|
-
const typedData = (data || []);
|
|
400
|
-
// Only update data if not currently searching
|
|
401
|
-
if (!isSearchingRef.current) {
|
|
402
|
-
setData(typedData);
|
|
403
|
-
}
|
|
404
|
-
return { data: typedData, error: null };
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
console.error('Error finding records:', err);
|
|
408
|
-
setError(err);
|
|
409
|
-
return { data: null, error: err };
|
|
410
|
-
}
|
|
411
|
-
finally {
|
|
412
|
-
setLoading(false);
|
|
413
|
-
}
|
|
414
|
-
}, []);
|
|
415
|
-
/**
|
|
416
|
-
* Find a single record by its unique identifier (usually ID).
|
|
417
|
-
*
|
|
418
|
-
* @param where - The unique identifier to find the record by
|
|
419
|
-
* @returns A promise with the found record or error
|
|
420
|
-
*
|
|
421
|
-
* @example
|
|
422
|
-
* // Find user by ID
|
|
423
|
-
* const result = await users.findUnique({ id: "123" });
|
|
424
|
-
* if (result.data) {
|
|
425
|
-
* console.log("Found user:", result.data.name);
|
|
426
|
-
* }
|
|
427
|
-
*/
|
|
428
|
-
const findUnique = (0, react_1.useCallback)(async (where) => {
|
|
429
|
-
try {
|
|
430
|
-
setLoading(true);
|
|
431
|
-
setError(null);
|
|
432
|
-
// Find the primary field (usually 'id')
|
|
433
|
-
// @ts-ignore: Supabase typing issue
|
|
434
|
-
const primaryKey = Object.keys(where)[0];
|
|
435
|
-
if (!primaryKey) {
|
|
436
|
-
throw new Error('A unique identifier is required');
|
|
437
|
-
}
|
|
438
|
-
const value = where[primaryKey];
|
|
439
|
-
if (value === undefined) {
|
|
440
|
-
throw new Error('A unique identifier is required');
|
|
441
|
-
}
|
|
442
|
-
const { data, error } = await supabase_client_1.supabase
|
|
443
|
-
.from(tableName)
|
|
444
|
-
.select('*')
|
|
445
|
-
.eq(primaryKey, value)
|
|
446
|
-
.maybeSingle();
|
|
447
|
-
if (error)
|
|
448
|
-
throw error;
|
|
449
|
-
return { data: data, error: null };
|
|
450
|
-
}
|
|
451
|
-
catch (err) {
|
|
452
|
-
console.error('Error finding unique record:', err);
|
|
453
|
-
setError(err);
|
|
454
|
-
return { data: null, error: err };
|
|
455
|
-
}
|
|
456
|
-
finally {
|
|
457
|
-
setLoading(false);
|
|
458
|
-
}
|
|
459
|
-
}, []);
|
|
460
|
-
// Set up realtime subscription for the list
|
|
461
|
-
(0, react_1.useEffect)(() => {
|
|
462
|
-
if (!realtime)
|
|
463
|
-
return;
|
|
464
|
-
// Clean up previous subscription if it exists
|
|
465
|
-
if (channelRef.current) {
|
|
466
|
-
channelRef.current.unsubscribe();
|
|
467
|
-
channelRef.current = null;
|
|
468
|
-
}
|
|
469
|
-
const channelId = channelName || `changes_to_${tableName}_${Math.random().toString(36).substring(2, 15)}`;
|
|
470
|
-
// Store the current filter and options for closure
|
|
471
|
-
const currentFilter = computedFilter;
|
|
472
|
-
const currentWhere = where;
|
|
473
|
-
const currentOrderBy = orderBy;
|
|
474
|
-
const currentLimit = limit;
|
|
475
|
-
const currentOffset = offset;
|
|
476
|
-
console.log(`Setting up subscription for ${tableName} with filter: ${currentFilter}`);
|
|
477
|
-
const channel = supabase_client_1.supabase
|
|
478
|
-
.channel(channelId)
|
|
479
|
-
.on('postgres_changes', {
|
|
480
|
-
event: '*',
|
|
481
|
-
schema: 'public',
|
|
482
|
-
table: tableName,
|
|
483
|
-
filter: currentFilter,
|
|
484
|
-
}, (payload) => {
|
|
485
|
-
console.log(`Received ${payload.eventType} event for ${tableName}`, payload);
|
|
486
|
-
// Skip realtime updates when search is active
|
|
487
|
-
if (isSearchingRef.current)
|
|
488
|
-
return;
|
|
489
|
-
if (payload.eventType === 'INSERT') {
|
|
490
|
-
// Process insert event
|
|
491
|
-
setData((prev) => {
|
|
492
|
-
try {
|
|
493
|
-
const newRecord = payload.new;
|
|
494
|
-
console.log(`Processing INSERT for ${tableName}`, { newRecord });
|
|
495
|
-
// Check if this record matches our filter if we have one
|
|
496
|
-
if (currentWhere) {
|
|
497
|
-
let matchesFilter = true;
|
|
498
|
-
// Check each filter condition
|
|
499
|
-
for (const [key, value] of Object.entries(currentWhere)) {
|
|
500
|
-
if (typeof value === 'object' && value !== null) {
|
|
501
|
-
// Complex filter - this is handled by Supabase, assume it matches
|
|
502
|
-
}
|
|
503
|
-
else if (newRecord[key] !== value) {
|
|
504
|
-
matchesFilter = false;
|
|
505
|
-
console.log(`Filter mismatch on ${key}`, { expected: value, actual: newRecord[key] });
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
if (!matchesFilter) {
|
|
510
|
-
console.log('New record does not match filter criteria, skipping');
|
|
511
|
-
return prev;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
// Check if record already exists (avoid duplicates)
|
|
515
|
-
const exists = prev.some(item =>
|
|
516
|
-
// @ts-ignore: Supabase typing issue
|
|
517
|
-
'id' in item && 'id' in newRecord && item.id === newRecord.id);
|
|
518
|
-
if (exists) {
|
|
519
|
-
console.log('Record already exists, skipping insertion');
|
|
520
|
-
return prev;
|
|
521
|
-
}
|
|
522
|
-
// Add the new record to the data
|
|
523
|
-
let newData = [newRecord, ...prev];
|
|
524
|
-
// Apply ordering if specified
|
|
525
|
-
if (currentOrderBy) {
|
|
526
|
-
const [orderField, direction] = Object.entries(currentOrderBy)[0] || [];
|
|
527
|
-
if (orderField) {
|
|
528
|
-
newData = [...newData].sort((a, b) => {
|
|
529
|
-
const aValue = a[orderField] ?? '';
|
|
530
|
-
const bValue = b[orderField] ?? '';
|
|
531
|
-
if (direction === 'asc') {
|
|
532
|
-
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// Apply limit if specified
|
|
541
|
-
if (currentLimit && currentLimit > 0) {
|
|
542
|
-
newData = newData.slice(0, currentLimit);
|
|
543
|
-
}
|
|
544
|
-
return newData;
|
|
545
|
-
}
|
|
546
|
-
catch (error) {
|
|
547
|
-
console.error('Error processing INSERT event:', error);
|
|
548
|
-
return prev;
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
else if (payload.eventType === 'UPDATE') {
|
|
553
|
-
// Process update event
|
|
554
|
-
setData((prev) => {
|
|
555
|
-
// Skip if search is active
|
|
556
|
-
if (isSearchingRef.current) {
|
|
557
|
-
return prev;
|
|
558
|
-
}
|
|
559
|
-
return prev.map((item) =>
|
|
560
|
-
// @ts-ignore: Supabase typing issue
|
|
561
|
-
'id' in item && 'id' in payload.new && item.id === payload.new.id
|
|
562
|
-
? payload.new
|
|
563
|
-
: item);
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
else if (payload.eventType === 'DELETE') {
|
|
567
|
-
// Process delete event
|
|
568
|
-
setData((prev) => {
|
|
569
|
-
// Skip if search is active
|
|
570
|
-
if (isSearchingRef.current) {
|
|
571
|
-
return prev;
|
|
572
|
-
}
|
|
573
|
-
// Save the current size before filtering
|
|
574
|
-
const currentSize = prev.length;
|
|
575
|
-
// Filter out the deleted item
|
|
576
|
-
const filteredData = prev.filter((item) => {
|
|
577
|
-
// @ts-ignore: Supabase typing issue
|
|
578
|
-
return !('id' in item && 'id' in payload.old && item.id === payload.old.id);
|
|
579
|
-
});
|
|
580
|
-
// If we need to maintain the size with a limit
|
|
581
|
-
if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) {
|
|
582
|
-
console.log(`Record deleted with limit ${currentLimit}, will fetch additional record to maintain size`);
|
|
583
|
-
// Use setTimeout to ensure this state update completes first
|
|
584
|
-
setTimeout(() => {
|
|
585
|
-
findMany({
|
|
586
|
-
where: currentWhere,
|
|
587
|
-
orderBy: currentOrderBy,
|
|
588
|
-
take: currentLimit,
|
|
589
|
-
skip: currentOffset
|
|
590
|
-
});
|
|
591
|
-
}, 0);
|
|
592
|
-
}
|
|
593
|
-
return filteredData;
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
})
|
|
597
|
-
.subscribe((status) => {
|
|
598
|
-
console.log(`Subscription status for ${tableName}`, status);
|
|
599
|
-
});
|
|
600
|
-
// Store the channel ref
|
|
601
|
-
channelRef.current = channel;
|
|
602
|
-
return () => {
|
|
603
|
-
console.log(`Unsubscribing from ${channelId}`);
|
|
604
|
-
if (channelRef.current) {
|
|
605
|
-
channelRef.current.unsubscribe();
|
|
606
|
-
channelRef.current = null;
|
|
607
|
-
}
|
|
608
|
-
if (searchTimeoutRef.current) {
|
|
609
|
-
clearTimeout(searchTimeoutRef.current);
|
|
610
|
-
searchTimeoutRef.current = null;
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
}, [realtime, channelName, computedFilter]);
|
|
614
|
-
// Create a memoized options object to prevent unnecessary re-renders
|
|
615
|
-
const optionsRef = (0, react_1.useRef)({ where, orderBy, limit, offset });
|
|
616
|
-
// Compare current options with previous options
|
|
617
|
-
const optionsChanged = (0, react_1.useCallback)(() => {
|
|
618
|
-
// Create stable string representations for deep comparison
|
|
619
|
-
const whereStr = where ? JSON.stringify(where) : '';
|
|
620
|
-
const orderByStr = orderBy ? JSON.stringify(orderBy) : '';
|
|
621
|
-
const prevWhereStr = optionsRef.current.where ? JSON.stringify(optionsRef.current.where) : '';
|
|
622
|
-
const prevOrderByStr = optionsRef.current.orderBy ? JSON.stringify(optionsRef.current.orderBy) : '';
|
|
623
|
-
// Compare the stable representations
|
|
624
|
-
const hasChanged = whereStr !== prevWhereStr ||
|
|
625
|
-
orderByStr !== prevOrderByStr ||
|
|
626
|
-
limit !== optionsRef.current.limit ||
|
|
627
|
-
offset !== optionsRef.current.offset;
|
|
628
|
-
if (hasChanged) {
|
|
629
|
-
// Update the ref with the new values
|
|
630
|
-
optionsRef.current = { where, orderBy, limit, offset };
|
|
631
|
-
return true;
|
|
632
|
-
}
|
|
633
|
-
return false;
|
|
634
|
-
}, [where, orderBy, limit, offset]);
|
|
635
|
-
// Load initial data based on options
|
|
636
|
-
(0, react_1.useEffect)(() => {
|
|
637
|
-
// Skip if search is active
|
|
638
|
-
if (isSearchingRef.current)
|
|
639
|
-
return;
|
|
640
|
-
// Skip if we've already loaded or if no filter criteria are provided
|
|
641
|
-
if (initialLoadRef.current) {
|
|
642
|
-
// Only reload if options have changed significantly
|
|
643
|
-
if (optionsChanged()) {
|
|
644
|
-
console.log(`Options changed for ${tableName}, reloading data`);
|
|
645
|
-
findMany({
|
|
646
|
-
where,
|
|
647
|
-
orderBy,
|
|
648
|
-
take: limit,
|
|
649
|
-
skip: offset
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
// Initial load
|
|
655
|
-
initialLoadRef.current = true;
|
|
656
|
-
findMany({
|
|
657
|
-
where,
|
|
658
|
-
orderBy,
|
|
659
|
-
take: limit,
|
|
660
|
-
skip: offset
|
|
661
|
-
});
|
|
662
|
-
}, [findMany, where, orderBy, limit, offset, optionsChanged]);
|
|
663
|
-
/**
|
|
664
|
-
* Create a new record with the provided data.
|
|
665
|
-
* Default values from the schema will be applied if not provided.
|
|
666
|
-
*
|
|
667
|
-
* @param data - The data to create the record with
|
|
668
|
-
* @returns A promise with the created record or error
|
|
669
|
-
*
|
|
670
|
-
* @example
|
|
671
|
-
* // Create a new user
|
|
672
|
-
* const result = await users.create({
|
|
673
|
-
* name: "John Doe",
|
|
674
|
-
* email: "john@example.com"
|
|
675
|
-
* });
|
|
676
|
-
*
|
|
677
|
-
* @example
|
|
678
|
-
* // Create with custom ID (overriding default)
|
|
679
|
-
* const result = await users.create({
|
|
680
|
-
* id: "custom-id-123",
|
|
681
|
-
* name: "John Doe"
|
|
682
|
-
* });
|
|
683
|
-
*/
|
|
684
|
-
const create = (0, react_1.useCallback)(async (data) => {
|
|
685
|
-
try {
|
|
686
|
-
setLoading(true);
|
|
687
|
-
setError(null);
|
|
688
|
-
const now = new Date().toISOString();
|
|
689
|
-
// Apply default values from schema
|
|
690
|
-
const appliedDefaults = {};
|
|
691
|
-
// Apply all default values that aren't already in the data
|
|
692
|
-
for (const [field, defaultValue] of Object.entries(defaultValues)) {
|
|
693
|
-
// @ts-ignore: Supabase typing issue
|
|
694
|
-
if (!(field in data)) {
|
|
695
|
-
// Parse the default value based on its type
|
|
696
|
-
if (defaultValue.includes('now()') || defaultValue.includes('now')) {
|
|
697
|
-
appliedDefaults[field] = now;
|
|
698
|
-
}
|
|
699
|
-
else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
|
|
700
|
-
appliedDefaults[field] = crypto.randomUUID();
|
|
701
|
-
}
|
|
702
|
-
else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
|
|
703
|
-
// Simple cuid-like implementation for client-side
|
|
704
|
-
appliedDefaults[field] = 'c' + Math.random().toString(36).substring(2, 15);
|
|
705
|
-
}
|
|
706
|
-
else if (defaultValue.includes('true')) {
|
|
707
|
-
appliedDefaults[field] = true;
|
|
708
|
-
}
|
|
709
|
-
else if (defaultValue.includes('false')) {
|
|
710
|
-
appliedDefaults[field] = false;
|
|
711
|
-
}
|
|
712
|
-
else if (!isNaN(Number(defaultValue))) {
|
|
713
|
-
// If it's a number
|
|
714
|
-
appliedDefaults[field] = Number(defaultValue);
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
// String or other value, remove quotes if present
|
|
718
|
-
const strValue = defaultValue.replace(/^["'](.*)["']$/, '$1');
|
|
719
|
-
appliedDefaults[field] = strValue;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const itemWithDefaults = {
|
|
724
|
-
...appliedDefaults, // Apply schema defaults first
|
|
725
|
-
...data, // Then user data (overrides defaults)
|
|
726
|
-
...(hasCreatedAt ? { created_at: now } : {}), // Always set timestamps
|
|
727
|
-
...(hasUpdatedAt ? { updated_at: now } : {})
|
|
728
|
-
};
|
|
729
|
-
const { data: result, error } = await supabase_client_1.supabase
|
|
730
|
-
.from(tableName)
|
|
731
|
-
.insert([itemWithDefaults])
|
|
732
|
-
.select();
|
|
733
|
-
if (error)
|
|
734
|
-
throw error;
|
|
735
|
-
// Return created record
|
|
736
|
-
return { data: result?.[0], error: null };
|
|
737
|
-
}
|
|
738
|
-
catch (err) {
|
|
739
|
-
console.error('Error creating record:', err);
|
|
740
|
-
setError(err);
|
|
741
|
-
return { data: null, error: err };
|
|
742
|
-
}
|
|
743
|
-
finally {
|
|
744
|
-
setLoading(false);
|
|
745
|
-
}
|
|
746
|
-
}, []);
|
|
747
|
-
/**
|
|
748
|
-
* Update an existing record identified by a unique identifier.
|
|
749
|
-
*
|
|
750
|
-
* @param params - Object containing the identifier and update data
|
|
751
|
-
* @returns A promise with the updated record or error
|
|
752
|
-
*
|
|
753
|
-
* @example
|
|
754
|
-
* // Update a user's name
|
|
755
|
-
* const result = await users.update({
|
|
756
|
-
* where: { id: "123" },
|
|
757
|
-
* data: { name: "New Name" }
|
|
758
|
-
* });
|
|
759
|
-
*
|
|
760
|
-
* @example
|
|
761
|
-
* // Update multiple fields
|
|
762
|
-
* const result = await users.update({
|
|
763
|
-
* where: { id: "123" },
|
|
764
|
-
* data: {
|
|
765
|
-
* name: "New Name",
|
|
766
|
-
* active: false
|
|
767
|
-
* }
|
|
768
|
-
* });
|
|
769
|
-
*/
|
|
770
|
-
const update = (0, react_1.useCallback)(async (params) => {
|
|
771
|
-
try {
|
|
772
|
-
setLoading(true);
|
|
773
|
-
setError(null);
|
|
774
|
-
// Find the primary field (usually 'id')
|
|
775
|
-
// @ts-ignore: Supabase typing issue
|
|
776
|
-
const primaryKey = Object.keys(params.where)[0];
|
|
777
|
-
if (!primaryKey) {
|
|
778
|
-
throw new Error('A unique identifier is required');
|
|
779
|
-
}
|
|
780
|
-
const value = params.where[primaryKey];
|
|
781
|
-
if (value === undefined) {
|
|
782
|
-
throw new Error('A unique identifier is required');
|
|
783
|
-
}
|
|
784
|
-
const now = new Date().toISOString();
|
|
785
|
-
// We do not apply default values for updates
|
|
786
|
-
// Default values are only for creation, not for updates,
|
|
787
|
-
// as existing records already have these values set
|
|
788
|
-
const itemWithDefaults = {
|
|
789
|
-
...params.data,
|
|
790
|
-
...(hasUpdatedAt ? { updated_at: now } : {})
|
|
791
|
-
};
|
|
792
|
-
const { data, error } = await supabase_client_1.supabase
|
|
793
|
-
.from(tableName)
|
|
794
|
-
.update(itemWithDefaults)
|
|
795
|
-
.eq(primaryKey, value)
|
|
796
|
-
.select();
|
|
797
|
-
if (error)
|
|
798
|
-
throw error;
|
|
799
|
-
// Return updated record
|
|
800
|
-
return { data: data?.[0], error: null };
|
|
801
|
-
}
|
|
802
|
-
catch (err) {
|
|
803
|
-
console.error('Error updating record:', err);
|
|
804
|
-
setError(err);
|
|
805
|
-
return { data: null, error: err };
|
|
806
|
-
}
|
|
807
|
-
finally {
|
|
808
|
-
setLoading(false);
|
|
809
|
-
}
|
|
810
|
-
}, []);
|
|
811
|
-
/**
|
|
812
|
-
* Delete a record by its unique identifier.
|
|
813
|
-
*
|
|
814
|
-
* @param where - The unique identifier to delete the record by
|
|
815
|
-
* @returns A promise with the deleted record or error
|
|
816
|
-
*
|
|
817
|
-
* @example
|
|
818
|
-
* // Delete a user by ID
|
|
819
|
-
* const result = await users.delete({ id: "123" });
|
|
820
|
-
* if (result.data) {
|
|
821
|
-
* console.log("Deleted user:", result.data.name);
|
|
822
|
-
* }
|
|
823
|
-
*/
|
|
824
|
-
const deleteRecord = (0, react_1.useCallback)(async (where) => {
|
|
825
|
-
try {
|
|
826
|
-
setLoading(true);
|
|
827
|
-
setError(null);
|
|
828
|
-
// Find the primary field (usually 'id')
|
|
829
|
-
// @ts-ignore: Supabase typing issue
|
|
830
|
-
const primaryKey = Object.keys(where)[0];
|
|
831
|
-
if (!primaryKey) {
|
|
832
|
-
throw new Error('A unique identifier is required');
|
|
833
|
-
}
|
|
834
|
-
const value = where[primaryKey];
|
|
835
|
-
if (value === undefined) {
|
|
836
|
-
throw new Error('A unique identifier is required');
|
|
837
|
-
}
|
|
838
|
-
// First fetch the record to return it
|
|
839
|
-
const { data: recordToDelete } = await supabase_client_1.supabase
|
|
840
|
-
.from(tableName)
|
|
841
|
-
.select('*')
|
|
842
|
-
.eq(primaryKey, value)
|
|
843
|
-
.maybeSingle();
|
|
844
|
-
if (!recordToDelete) {
|
|
845
|
-
throw new Error('Record not found');
|
|
846
|
-
}
|
|
847
|
-
// Then delete it
|
|
848
|
-
const { error } = await supabase_client_1.supabase
|
|
849
|
-
.from(tableName)
|
|
850
|
-
.delete()
|
|
851
|
-
.eq(primaryKey, value);
|
|
852
|
-
if (error)
|
|
853
|
-
throw error;
|
|
854
|
-
// Return the deleted record
|
|
855
|
-
return { data: recordToDelete, error: null };
|
|
856
|
-
}
|
|
857
|
-
catch (err) {
|
|
858
|
-
console.error('Error deleting record:', err);
|
|
859
|
-
setError(err);
|
|
860
|
-
return { data: null, error: err };
|
|
861
|
-
}
|
|
862
|
-
finally {
|
|
863
|
-
setLoading(false);
|
|
864
|
-
}
|
|
865
|
-
}, []);
|
|
866
|
-
/**
|
|
867
|
-
* Delete multiple records matching the filter criteria.
|
|
868
|
-
*
|
|
869
|
-
* @param params - Query parameters for filtering records to delete
|
|
870
|
-
* @returns A promise with the count of deleted records or error
|
|
871
|
-
*
|
|
872
|
-
* @example
|
|
873
|
-
* // Delete all inactive users
|
|
874
|
-
* const result = await users.deleteMany({
|
|
875
|
-
* where: { active: false }
|
|
876
|
-
* });
|
|
877
|
-
* console.log(`Deleted ${result.count} inactive users`);
|
|
878
|
-
*
|
|
879
|
-
* @example
|
|
880
|
-
* // Delete all records (use with caution!)
|
|
881
|
-
* const result = await users.deleteMany();
|
|
882
|
-
*/
|
|
883
|
-
const deleteMany = (0, react_1.useCallback)(async (params) => {
|
|
884
|
-
try {
|
|
885
|
-
setLoading(true);
|
|
886
|
-
setError(null);
|
|
887
|
-
// First, get the records that will be deleted to count them
|
|
888
|
-
let query = supabase_client_1.supabase.from(tableName).select('*');
|
|
889
|
-
// Apply where conditions if provided
|
|
890
|
-
if (params?.where) {
|
|
891
|
-
query = applyFilter(query, params.where);
|
|
892
|
-
}
|
|
893
|
-
// Get records that will be deleted
|
|
894
|
-
const { data: recordsToDelete, error: fetchError } = await query;
|
|
895
|
-
if (fetchError)
|
|
896
|
-
throw fetchError;
|
|
897
|
-
if (!recordsToDelete?.length) {
|
|
898
|
-
return { count: 0, error: null };
|
|
899
|
-
}
|
|
900
|
-
// Build the delete query
|
|
901
|
-
let deleteQuery = supabase_client_1.supabase.from(tableName).delete();
|
|
902
|
-
// Apply the same filter to the delete operation
|
|
903
|
-
if (params?.where) {
|
|
904
|
-
// @ts-ignore: Supabase typing issue
|
|
905
|
-
deleteQuery = applyFilter(deleteQuery, params.where);
|
|
906
|
-
}
|
|
907
|
-
// Perform the delete
|
|
908
|
-
const { error: deleteError } = await deleteQuery;
|
|
909
|
-
if (deleteError)
|
|
910
|
-
throw deleteError;
|
|
911
|
-
// Return the count of deleted records
|
|
912
|
-
return { count: recordsToDelete.length, error: null };
|
|
913
|
-
}
|
|
914
|
-
catch (err) {
|
|
915
|
-
console.error('Error deleting multiple records:', err);
|
|
916
|
-
setError(err);
|
|
917
|
-
return { count: 0, error: err };
|
|
918
|
-
}
|
|
919
|
-
finally {
|
|
920
|
-
setLoading(false);
|
|
921
|
-
}
|
|
922
|
-
}, []);
|
|
923
|
-
/**
|
|
924
|
-
* Find the first record matching the filter criteria.
|
|
925
|
-
*
|
|
926
|
-
* @param params - Query parameters for filtering and ordering
|
|
927
|
-
* @returns A promise with the first matching record or error
|
|
928
|
-
*
|
|
929
|
-
* @example
|
|
930
|
-
* // Find the first admin user
|
|
931
|
-
* const result = await users.findFirst({
|
|
932
|
-
* where: { role: 'admin' }
|
|
933
|
-
* });
|
|
934
|
-
*
|
|
935
|
-
* @example
|
|
936
|
-
* // Find the oldest post
|
|
937
|
-
* const result = await posts.findFirst({
|
|
938
|
-
* orderBy: { created_at: 'asc' }
|
|
939
|
-
* });
|
|
940
|
-
*/
|
|
941
|
-
const findFirst = (0, react_1.useCallback)(async (params) => {
|
|
942
|
-
try {
|
|
943
|
-
const result = await findMany({
|
|
944
|
-
...params,
|
|
945
|
-
take: 1
|
|
946
|
-
});
|
|
947
|
-
if (result.error)
|
|
948
|
-
return { data: null, error: result.error };
|
|
949
|
-
if (!result.data.length)
|
|
950
|
-
return { data: null, error: new Error('No records found') };
|
|
951
|
-
// @ts-ignore: Supabase typing issue
|
|
952
|
-
return { data: result.data[0], error: null };
|
|
953
|
-
}
|
|
954
|
-
catch (err) {
|
|
955
|
-
console.error('Error finding first record:', err);
|
|
956
|
-
return { data: null, error: err };
|
|
957
|
-
}
|
|
958
|
-
}, [findMany]);
|
|
959
|
-
/**
|
|
960
|
-
* Create a record if it doesn't exist, or update it if it does.
|
|
961
|
-
*
|
|
962
|
-
* @param params - Object containing the identifier, update data, and create data
|
|
963
|
-
* @returns A promise with the created or updated record or error
|
|
964
|
-
*
|
|
965
|
-
* @example
|
|
966
|
-
* // Upsert a user by ID
|
|
967
|
-
* const result = await users.upsert({
|
|
968
|
-
* where: { id: "123" },
|
|
969
|
-
* update: { lastLogin: new Date().toISOString() },
|
|
970
|
-
* create: {
|
|
971
|
-
* id: "123",
|
|
972
|
-
* name: "John Doe",
|
|
973
|
-
* email: "john@example.com",
|
|
974
|
-
* lastLogin: new Date().toISOString()
|
|
975
|
-
* }
|
|
976
|
-
* });
|
|
977
|
-
*/
|
|
978
|
-
const upsert = (0, react_1.useCallback)(async (params) => {
|
|
979
|
-
try {
|
|
980
|
-
// Check if record exists
|
|
981
|
-
const { data: existing } = await findUnique(params.where);
|
|
982
|
-
// Update if exists, otherwise create
|
|
983
|
-
if (existing) {
|
|
984
|
-
return update({ where: params.where, data: params.update });
|
|
985
|
-
}
|
|
986
|
-
else {
|
|
987
|
-
return create(params.create);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
catch (err) {
|
|
991
|
-
console.error('Error upserting record:', err);
|
|
992
|
-
return { data: null, error: err };
|
|
993
|
-
}
|
|
994
|
-
}, [findUnique, update, create]);
|
|
995
|
-
/**
|
|
996
|
-
* Count the number of records matching the filter criteria.
|
|
997
|
-
*
|
|
998
|
-
* @param params - Query parameters for filtering
|
|
999
|
-
* @returns A promise with the count of matching records
|
|
1000
|
-
*
|
|
1001
|
-
* @example
|
|
1002
|
-
* // Count all users
|
|
1003
|
-
* const count = await users.count();
|
|
1004
|
-
*
|
|
1005
|
-
* @example
|
|
1006
|
-
* // Count active users
|
|
1007
|
-
* const activeCount = await users.count({
|
|
1008
|
-
* where: { active: true }
|
|
1009
|
-
* });
|
|
1010
|
-
*/
|
|
1011
|
-
const count = (0, react_1.useCallback)(async (params) => {
|
|
1012
|
-
try {
|
|
1013
|
-
let query = supabase_client_1.supabase.from(tableName).select('*', { count: 'exact', head: true });
|
|
1014
|
-
// Use provided where filter, or fall back to the hook's original where filter
|
|
1015
|
-
const effectiveWhere = params?.where ?? where;
|
|
1016
|
-
if (effectiveWhere) {
|
|
1017
|
-
query = applyFilter(query, effectiveWhere);
|
|
1018
|
-
}
|
|
1019
|
-
const { count, error } = await query;
|
|
1020
|
-
if (error)
|
|
1021
|
-
throw error;
|
|
1022
|
-
return count || 0;
|
|
1023
|
-
}
|
|
1024
|
-
catch (err) {
|
|
1025
|
-
console.error('Error counting records:', err);
|
|
1026
|
-
return 0;
|
|
1027
|
-
}
|
|
1028
|
-
}, [where]);
|
|
1029
|
-
/**
|
|
1030
|
-
* Manually refresh the data with current filter settings.
|
|
1031
|
-
* Useful after external operations or when realtime is disabled.
|
|
1032
|
-
*
|
|
1033
|
-
* @param params - Optional override parameters for this specific refresh
|
|
1034
|
-
* @returns A promise with the refreshed data or error
|
|
1035
|
-
*
|
|
1036
|
-
* @example
|
|
1037
|
-
* // Refresh with current filter settings
|
|
1038
|
-
* await users.refresh();
|
|
1039
|
-
*
|
|
1040
|
-
* @example
|
|
1041
|
-
* // Refresh with different filters for this call only
|
|
1042
|
-
* await users.refresh({
|
|
1043
|
-
* where: { active: true },
|
|
1044
|
-
* orderBy: { name: 'asc' }
|
|
1045
|
-
* });
|
|
1046
|
-
*/
|
|
1047
|
-
const refresh = (0, react_1.useCallback)((params) => {
|
|
1048
|
-
// If search is active, refresh search results
|
|
1049
|
-
if (isSearchingRef.current && searchQueries.length > 0) {
|
|
1050
|
-
executeSearch(searchQueries);
|
|
1051
|
-
return Promise.resolve({ data: data, error: null });
|
|
1052
|
-
}
|
|
1053
|
-
// Otherwise, refresh normal data using original params if not explicitly overridden
|
|
1054
|
-
return findMany({
|
|
1055
|
-
where: params?.where ?? where,
|
|
1056
|
-
orderBy: params?.orderBy ?? orderBy,
|
|
1057
|
-
take: params?.take ?? limit,
|
|
1058
|
-
skip: params?.skip ?? offset
|
|
1059
|
-
});
|
|
1060
|
-
}, [findMany, data, searchQueries, where, orderBy, limit, offset]);
|
|
1061
|
-
// Construct final hook API with or without search
|
|
1062
|
-
const api = {
|
|
1063
|
-
// State
|
|
1064
|
-
data,
|
|
1065
|
-
error,
|
|
1066
|
-
loading,
|
|
1067
|
-
// Finder methods
|
|
1068
|
-
findUnique,
|
|
1069
|
-
findMany,
|
|
1070
|
-
findFirst,
|
|
1071
|
-
// Mutation methods
|
|
1072
|
-
create,
|
|
1073
|
-
update,
|
|
1074
|
-
delete: deleteRecord,
|
|
1075
|
-
deleteMany,
|
|
1076
|
-
upsert,
|
|
1077
|
-
// Utilities
|
|
1078
|
-
count,
|
|
1079
|
-
// Manual refresh
|
|
1080
|
-
refresh
|
|
1081
|
-
};
|
|
1082
|
-
// Add search object if searchable fields are present
|
|
1083
|
-
return searchFields.length > 0
|
|
1084
|
-
? {
|
|
1085
|
-
...api,
|
|
1086
|
-
search
|
|
1087
|
-
}
|
|
1088
|
-
: api;
|
|
1089
|
-
};
|
|
1090
|
-
}
|