suparisma 0.0.1 → 0.0.2

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