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.
@@ -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
- }