suparisma 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.gitattributes +2 -0
  2. package/LICENSE +21 -0
  3. package/README.md +4 -0
  4. package/dist/config.js +7 -0
  5. package/dist/generated/supabase-client-generated.js +7 -0
  6. package/dist/generators/coreGenerator.js +1430 -0
  7. package/dist/generators/hookGenerator.js +108 -0
  8. package/dist/generators/indexGenerator.js +100 -0
  9. package/dist/generators/supabaseClientGenerator.js +29 -0
  10. package/dist/generators/typeGenerator.js +566 -0
  11. package/dist/hooks/generated/UserTypes.js +2 -0
  12. package/dist/hooks/generated/core.js +1089 -0
  13. package/dist/hooks/generated/index.js +33 -0
  14. package/dist/hooks/generated/useSuparismaUser.js +60 -0
  15. package/dist/index.js +259 -0
  16. package/dist/parser.js +117 -0
  17. package/dist/types.js +2 -0
  18. package/package.json +28 -0
  19. package/prisma/schema.prisma +22 -0
  20. package/src/config.ts +7 -0
  21. package/src/generated/hooks/useSuparismaUser.ts +77 -0
  22. package/src/generated/index.ts +50 -0
  23. package/src/generated/types/UserTypes.ts +400 -0
  24. package/src/generated/utils/core.ts +1413 -0
  25. package/src/generated/utils/supabase-client.ts +7 -0
  26. package/src/generators/coreGenerator.ts +1426 -0
  27. package/src/generators/hookGenerator.ts +110 -0
  28. package/src/generators/indexGenerator.ts +117 -0
  29. package/src/generators/supabaseClientGenerator.ts +24 -0
  30. package/src/generators/typeGenerator.ts +587 -0
  31. package/src/index.ts +339 -0
  32. package/src/parser.ts +134 -0
  33. package/src/suparisma/generated/UserTypes.ts +400 -0
  34. package/src/suparisma/generated/core.ts +1413 -0
  35. package/src/suparisma/generated/hooks/useSuparismaUser.ts +77 -0
  36. package/src/suparisma/generated/index.ts +50 -0
  37. package/src/suparisma/generated/supabase-client-generated.ts +9 -0
  38. package/src/suparisma/generated/types/UserTypes.ts +400 -0
  39. package/src/suparisma/generated/useSuparismaUser.ts +77 -0
  40. package/src/suparisma/generated/utils/core.ts +1413 -0
  41. package/src/suparisma/generated/utils/supabase-client.ts +7 -0
  42. package/src/types.ts +57 -0
  43. package/tsconfig.json +20 -0
@@ -0,0 +1,1430 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateCoreFile = generateCoreFile;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../config");
10
+ /**
11
+ * Generate core hook factory file
12
+ */
13
+ function generateCoreFile() {
14
+ const coreContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
15
+ // Edit the generator script instead: scripts/generate-realtime-hooks.ts
16
+
17
+ import { useEffect, useState, useCallback, useRef } from 'react';
18
+ import { supabase } from '../supabase-client-generated';
19
+
20
+ /**
21
+ * Represents a single search query against a field
22
+ * @example
23
+ * // Search for users with names containing "john"
24
+ * const query = { field: "name", value: "john" };
25
+ */
26
+ export type SearchQuery = {
27
+ /** The field name to search in */
28
+ field: string;
29
+ /** The search term/value to look for */
30
+ value: string;
31
+ };
32
+
33
+ // Define type for Supabase query builder
34
+ export type SupabaseQueryBuilder = ReturnType<ReturnType<typeof supabase.from>['select']>;
35
+
36
+ /**
37
+ * Advanced filter operators for complex queries
38
+ * @example
39
+ * // Users older than 21
40
+ * { age: { gt: 21 } }
41
+ *
42
+ * @example
43
+ * // Posts with titles containing "news"
44
+ * { title: { contains: "news" } }
45
+ */
46
+ export type FilterOperators<T> = {
47
+ /** Equal to value */
48
+ equals?: T;
49
+ /** Not equal to value */
50
+ not?: T;
51
+ /** Value is in the array */
52
+ in?: T[];
53
+ /** Value is not in the array */
54
+ notIn?: T[];
55
+ /** Less than value */
56
+ lt?: T;
57
+ /** Less than or equal to value */
58
+ lte?: T;
59
+ /** Greater than value */
60
+ gt?: T;
61
+ /** Greater than or equal to value */
62
+ gte?: T;
63
+ /** String contains value (case insensitive) */
64
+ contains?: string;
65
+ /** String starts with value (case insensitive) */
66
+ startsWith?: string;
67
+ /** String ends with value (case insensitive) */
68
+ endsWith?: string;
69
+ };
70
+
71
+ // Type for a single field in an advanced where filter
72
+ export type AdvancedWhereInput<T> = {
73
+ [K in keyof T]?: T[K] | FilterOperators<T[K]>;
74
+ };
75
+
76
+ /**
77
+ * Configuration options for the Suparisma hooks
78
+ * @example
79
+ * // Basic usage
80
+ * const { data } = useSuparismaUser();
81
+ *
82
+ * @example
83
+ * // With filtering
84
+ * const { data } = useSuparismaUser({
85
+ * where: { age: { gt: 21 } }
86
+ * });
87
+ *
88
+ * @example
89
+ * // With ordering and limits
90
+ * const { data } = useSuparismaUser({
91
+ * orderBy: { created_at: 'desc' },
92
+ * limit: 10
93
+ * });
94
+ */
95
+ export type SuparismaOptions<TWhereInput, TOrderByInput> = {
96
+ /** Whether to enable realtime updates (default: true) */
97
+ realtime?: boolean;
98
+ /** Custom channel name for realtime subscription */
99
+ channelName?: string;
100
+ /** Type-safe filter for queries and realtime events */
101
+ where?: TWhereInput;
102
+ /** Legacy string filter (use 'where' instead for type safety) */
103
+ realtimeFilter?: string;
104
+ /** Type-safe ordering for queries */
105
+ orderBy?: TOrderByInput;
106
+ /** Limit the number of records returned */
107
+ limit?: number;
108
+ /** Offset for pagination (skip records) */
109
+ offset?: number;
110
+ };
111
+
112
+ /**
113
+ * Return type for database operations
114
+ * @example
115
+ * const result = await users.create({ name: "John" });
116
+ * if (result.error) {
117
+ * console.error(result.error);
118
+ * } else {
119
+ * console.log(result.data);
120
+ * }
121
+ */
122
+ export type ModelResult<T> = Promise<{
123
+ data: T;
124
+ error: null;
125
+ } | {
126
+ data: null;
127
+ error: Error;
128
+ }>;
129
+
130
+ /**
131
+ * Complete search state and methods for searchable models
132
+ * @example
133
+ * // Search for users with name containing "john"
134
+ * users.search.addQuery({ field: "name", value: "john" });
135
+ *
136
+ * @example
137
+ * // Check if search is loading
138
+ * if (users.search.loading) {
139
+ * return <div>Searching...</div>;
140
+ * }
141
+ */
142
+ export type SearchState = {
143
+ /** Current active search queries */
144
+ queries: SearchQuery[];
145
+ /** Whether a search is currently in progress */
146
+ loading: boolean;
147
+ /** Replace all search queries with a new set */
148
+ setQueries: (queries: SearchQuery[]) => void;
149
+ /** Add a new search query (replaces existing query for same field) */
150
+ addQuery: (query: SearchQuery) => void;
151
+ /** Remove a search query by field name */
152
+ removeQuery: (field: string) => void;
153
+ /** Clear all search queries and return to normal data fetching */
154
+ clearQueries: () => void;
155
+ };
156
+
157
+ /**
158
+ * Convert a type-safe where filter to Supabase filter string
159
+ */
160
+ export function buildFilterString<T>(where?: T): string | undefined {
161
+ if (!where) return undefined;
162
+
163
+ const filters: string[] = [];
164
+
165
+ for (const [key, value] of Object.entries(where)) {
166
+ if (value !== undefined) {
167
+ if (typeof value === 'object' && value !== null) {
168
+ // Handle advanced operators
169
+ const advancedOps = value as unknown as FilterOperators<any>;
170
+
171
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
172
+ filters.push(\`\${key}=eq.\${advancedOps.equals}\`);
173
+ }
174
+
175
+ if ('not' in advancedOps && advancedOps.not !== undefined) {
176
+ filters.push(\`\${key}=neq.\${advancedOps.not}\`);
177
+ }
178
+
179
+ if ('gt' in advancedOps && advancedOps.gt !== undefined) {
180
+ filters.push(\`\${key}=gt.\${advancedOps.gt}\`);
181
+ }
182
+
183
+ if ('gte' in advancedOps && advancedOps.gte !== undefined) {
184
+ filters.push(\`\${key}=gte.\${advancedOps.gte}\`);
185
+ }
186
+
187
+ if ('lt' in advancedOps && advancedOps.lt !== undefined) {
188
+ filters.push(\`\${key}=lt.\${advancedOps.lt}\`);
189
+ }
190
+
191
+ if ('lte' in advancedOps && advancedOps.lte !== undefined) {
192
+ filters.push(\`\${key}=lte.\${advancedOps.lte}\`);
193
+ }
194
+
195
+ if ('in' in advancedOps && advancedOps.in?.length) {
196
+ filters.push(\`\${key}=in.(\${advancedOps.in.join(',')})\`);
197
+ }
198
+
199
+ if ('contains' in advancedOps && advancedOps.contains !== undefined) {
200
+ filters.push(\`\${key}=ilike.*\${advancedOps.contains}*\`);
201
+ }
202
+
203
+ if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
204
+ filters.push(\`\${key}=ilike.\${advancedOps.startsWith}%\`);
205
+ }
206
+
207
+ if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
208
+ filters.push(\`\${key}=ilike.%\${advancedOps.endsWith}\`);
209
+ }
210
+ } else {
211
+ // Simple equality
212
+ filters.push(\`\${key}=eq.\${value}\`);
213
+ }
214
+ }
215
+ }
216
+
217
+ return filters.length > 0 ? filters.join(',') : undefined;
218
+ }
219
+
220
+ /**
221
+ * Apply filter to the query builder
222
+ */
223
+ export function applyFilter<T>(
224
+ query: SupabaseQueryBuilder,
225
+ where: T
226
+ ): SupabaseQueryBuilder {
227
+ if (!where) return query;
228
+
229
+ let filteredQuery = query;
230
+
231
+ // Apply each filter condition
232
+ for (const [key, value] of Object.entries(where)) {
233
+ if (value !== undefined) {
234
+ if (typeof value === 'object' && value !== null) {
235
+ // Handle advanced operators
236
+ const advancedOps = value as unknown as FilterOperators<any>;
237
+
238
+ if ('equals' in advancedOps && advancedOps.equals !== undefined) {
239
+ // @ts-ignore: Supabase typing issue
240
+ filteredQuery = filteredQuery.eq(key, advancedOps.equals);
241
+ }
242
+
243
+ if ('not' in advancedOps && advancedOps.not !== undefined) {
244
+ // @ts-ignore: Supabase typing issue
245
+ filteredQuery = filteredQuery.neq(key, advancedOps.not);
246
+ }
247
+
248
+ if ('gt' in advancedOps && advancedOps.gt !== undefined) {
249
+ // @ts-ignore: Supabase typing issue
250
+ filteredQuery = filteredQuery.gt(key, advancedOps.gt);
251
+ }
252
+
253
+ if ('gte' in advancedOps && advancedOps.gte !== undefined) {
254
+ // @ts-ignore: Supabase typing issue
255
+ filteredQuery = filteredQuery.gte(key, advancedOps.gte);
256
+ }
257
+
258
+ if ('lt' in advancedOps && advancedOps.lt !== undefined) {
259
+ // @ts-ignore: Supabase typing issue
260
+ filteredQuery = filteredQuery.lt(key, advancedOps.lt);
261
+ }
262
+
263
+ if ('lte' in advancedOps && advancedOps.lte !== undefined) {
264
+ // @ts-ignore: Supabase typing issue
265
+ filteredQuery = filteredQuery.lte(key, advancedOps.lte);
266
+ }
267
+
268
+ if ('in' in advancedOps && advancedOps.in?.length) {
269
+ // @ts-ignore: Supabase typing issue
270
+ filteredQuery = filteredQuery.in(key, advancedOps.in);
271
+ }
272
+
273
+ if ('contains' in advancedOps && advancedOps.contains !== undefined) {
274
+ // @ts-ignore: Supabase typing issue
275
+ filteredQuery = filteredQuery.ilike(key, \`*\${advancedOps.contains}*\`);
276
+ }
277
+
278
+ if ('startsWith' in advancedOps && advancedOps.startsWith !== undefined) {
279
+ // @ts-ignore: Supabase typing issue
280
+ filteredQuery = filteredQuery.ilike(key, \`\${advancedOps.startsWith}%\`);
281
+ }
282
+
283
+ if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
284
+ // @ts-ignore: Supabase typing issue
285
+ filteredQuery = filteredQuery.ilike(key, \`%\${advancedOps.endsWith}\`);
286
+ }
287
+ } else {
288
+ // Simple equality
289
+ // @ts-ignore: Supabase typing issue
290
+ filteredQuery = filteredQuery.eq(key, value);
291
+ }
292
+ }
293
+ }
294
+
295
+ return filteredQuery;
296
+ }
297
+
298
+ /**
299
+ * Apply order by to the query builder
300
+ */
301
+ export function applyOrderBy<T>(
302
+ query: SupabaseQueryBuilder,
303
+ orderBy?: T,
304
+ hasCreatedAt?: boolean
305
+ ): SupabaseQueryBuilder {
306
+ if (!orderBy) {
307
+ // By default, sort by created_at if available
308
+ if (hasCreatedAt) {
309
+ // @ts-ignore: Supabase typing issue
310
+ return query.order('created_at', { ascending: false });
311
+ }
312
+ return query;
313
+ }
314
+
315
+ // Apply each order by clause
316
+ let orderedQuery = query;
317
+ for (const [key, direction] of Object.entries(orderBy)) {
318
+ // @ts-ignore: Supabase typing issue
319
+ orderedQuery = orderedQuery.order(key, {
320
+ ascending: direction === 'asc'
321
+ });
322
+ }
323
+
324
+ return orderedQuery;
325
+ }
326
+
327
+ /**
328
+ * Core hook factory function that creates a type-safe realtime hook for a specific model.
329
+ * This is the foundation for all Suparisma hooks.
330
+ */
331
+ export function createSuparismaHook<
332
+ TModel,
333
+ TWithRelations,
334
+ TCreateInput,
335
+ TUpdateInput,
336
+ TWhereInput,
337
+ TWhereUniqueInput,
338
+ TOrderByInput
339
+ >(config: {
340
+ tableName: string;
341
+ hasCreatedAt: boolean;
342
+ hasUpdatedAt: boolean;
343
+ searchFields?: string[];
344
+ defaultValues?: Record<string, string>; // Added parameter for default values
345
+ }) {
346
+ const { tableName, hasCreatedAt, hasUpdatedAt, searchFields = [], defaultValues = {} } = config;
347
+
348
+ /**
349
+ * The main hook function that provides all data access methods for a model.
350
+ *
351
+ * @param options - Optional configuration for data fetching, filtering, and realtime
352
+ *
353
+ * @returns An API object with data state and CRUD methods
354
+ *
355
+ * @example
356
+ * // Basic usage
357
+ * const users = useSuparismaUser();
358
+ * const { data, loading, error } = users;
359
+ *
360
+ * @example
361
+ * // With filtering
362
+ * const users = useSuparismaUser({
363
+ * where: { role: 'admin' },
364
+ * orderBy: { created_at: 'desc' }
365
+ * });
366
+ */
367
+ return function useSuparismaHook(options: SuparismaOptions<TWhereInput, TOrderByInput> = {}) {
368
+ const {
369
+ realtime = true,
370
+ channelName,
371
+ where,
372
+ realtimeFilter,
373
+ orderBy,
374
+ limit,
375
+ offset,
376
+ } = options;
377
+
378
+ // Compute the actual filter string from the type-safe where object or use legacy filter
379
+ const computedFilter = where ? buildFilterString(where) : realtimeFilter;
380
+
381
+ // Single data collection for holding results
382
+ const [data, setData] = useState<TWithRelations[]>([]);
383
+ const [error, setError] = useState<Error | null>(null);
384
+ const [loading, setLoading] = useState<boolean>(false);
385
+
386
+ // Search state
387
+ const [searchQueries, setSearchQueries] = useState<SearchQuery[]>([]);
388
+ const [searchLoading, setSearchLoading] = useState<boolean>(false);
389
+
390
+ const initialLoadRef = useRef(false);
391
+ const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
392
+ const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
393
+ const isSearchingRef = useRef<boolean>(false);
394
+
395
+ // Create the search state object with all required methods
396
+ const search: SearchState = {
397
+ queries: searchQueries,
398
+ loading: searchLoading,
399
+
400
+ // Set all search queries at once
401
+ setQueries: useCallback((queries: SearchQuery[]) => {
402
+ // Validate that all fields are searchable
403
+ const validQueries = queries.filter(query =>
404
+ searchFields.includes(query.field) && query.value.trim() !== ''
405
+ );
406
+
407
+ setSearchQueries(validQueries);
408
+
409
+ // Execute search if there are valid queries
410
+ if (validQueries.length > 0) {
411
+ executeSearch(validQueries);
412
+ } else {
413
+ // If no valid queries, reset to normal data fetching
414
+ isSearchingRef.current = false;
415
+ findMany({ where, orderBy, take: limit, skip: offset });
416
+ }
417
+ }, [where, orderBy, limit, offset]),
418
+
419
+ // Add a single search query
420
+ addQuery: useCallback((query: SearchQuery) => {
421
+ // Validate that the field is searchable
422
+ if (!searchFields.includes(query.field) || query.value.trim() === '') {
423
+ return;
424
+ }
425
+
426
+ setSearchQueries(prev => {
427
+ // Replace if query for this field already exists, otherwise add
428
+ const exists = prev.some(q => q.field === query.field);
429
+ const newQueries = exists
430
+ ? prev.map(q => q.field === query.field ? query : q)
431
+ : [...prev, query];
432
+
433
+ // Execute search with updated queries
434
+ executeSearch(newQueries);
435
+
436
+ return newQueries;
437
+ });
438
+ }, []),
439
+
440
+ // Remove a search query by field
441
+ removeQuery: useCallback((field: string) => {
442
+ setSearchQueries(prev => {
443
+ const newQueries = prev.filter(q => q.field !== field);
444
+
445
+ // If we still have queries, execute search with remaining queries
446
+ if (newQueries.length > 0) {
447
+ executeSearch(newQueries);
448
+ } else {
449
+ // If no queries left, reset to normal data fetching
450
+ isSearchingRef.current = false;
451
+ findMany({ where, orderBy, take: limit, skip: offset });
452
+ }
453
+
454
+ return newQueries;
455
+ });
456
+ }, [where, orderBy, limit, offset]),
457
+
458
+ // Clear all search queries
459
+ clearQueries: useCallback(() => {
460
+ setSearchQueries([]);
461
+ isSearchingRef.current = false;
462
+ findMany({ where, orderBy, take: limit, skip: offset });
463
+ }, [where, orderBy, limit, offset])
464
+ };
465
+
466
+ // Execute search based on queries
467
+ const executeSearch = useCallback(async (queries: SearchQuery[]) => {
468
+ // Clear the previous timeout
469
+ if (searchTimeoutRef.current) {
470
+ clearTimeout(searchTimeoutRef.current);
471
+ }
472
+
473
+ // Skip if no searchable fields or no valid queries
474
+ if (searchFields.length === 0 || queries.length === 0) {
475
+ return;
476
+ }
477
+
478
+ setSearchLoading(true);
479
+ isSearchingRef.current = true;
480
+
481
+ // Use debounce to prevent rapid searches
482
+ searchTimeoutRef.current = setTimeout(async () => {
483
+ try {
484
+ let results: TWithRelations[] = [];
485
+
486
+ // Execute RPC function for each query using Promise.all
487
+ const searchPromises = queries.map(query => {
488
+ // Build function name: search_tablename_by_fieldname_prefix
489
+ const functionName = \`search_\${tableName}_by_\${query.field}_prefix\`;
490
+
491
+ // Call RPC function
492
+ return supabase.rpc(functionName, { prefix: query.value.trim() });
493
+ });
494
+
495
+ // Execute all search queries in parallel
496
+ const searchResults = await Promise.all(searchPromises);
497
+
498
+ // Combine and deduplicate results
499
+ const allResults: Record<string, TWithRelations> = {};
500
+
501
+ // Process each search result
502
+ searchResults.forEach((result, index) => {
503
+ if (result.error) {
504
+ console.error(\`Search error for \${queries[index]?.field}:\`, result.error);
505
+ return;
506
+ }
507
+
508
+ if (result.data) {
509
+ // Add each result, using id as key to deduplicate
510
+ for (const item of result.data as TWithRelations[]) {
511
+ // @ts-ignore: Assume item has an id property
512
+ if (item.id) {
513
+ // @ts-ignore: Add to results using id as key
514
+ allResults[item.id] = item;
515
+ }
516
+ }
517
+ }
518
+ });
519
+
520
+ // Convert back to array
521
+ results = Object.values(allResults);
522
+
523
+ // Apply any where conditions client-side
524
+ if (where) {
525
+ results = results.filter((item) => {
526
+ for (const [key, value] of Object.entries(where)) {
527
+ if (typeof value === 'object' && value !== null) {
528
+ // Skip complex filters for now
529
+ continue;
530
+ }
531
+
532
+ if (item[key as keyof typeof item] !== value) {
533
+ return false;
534
+ }
535
+ }
536
+ return true;
537
+ });
538
+ }
539
+
540
+ // Apply ordering if needed
541
+ if (orderBy) {
542
+ const orderEntries = Object.entries(orderBy);
543
+ if (orderEntries.length > 0) {
544
+ const [orderField, direction] = orderEntries[0] || [];
545
+ results = [...results].sort((a, b) => {
546
+ const aValue = a[orderField as keyof typeof a] ?? '';
547
+ const bValue = b[orderField as keyof typeof b] ?? '';
548
+
549
+ if (direction === 'asc') {
550
+ return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
551
+ } else {
552
+ return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
553
+ }
554
+ });
555
+ }
556
+ }
557
+
558
+ // Apply pagination if needed
559
+ if (limit && limit > 0) {
560
+ results = results.slice(0, limit);
561
+ }
562
+
563
+ if (offset && offset > 0) {
564
+ results = results.slice(offset);
565
+ }
566
+
567
+ // Update data with search results
568
+ setData(results);
569
+ } catch (err) {
570
+ console.error('Search error:', err);
571
+ setError(err as Error);
572
+ } finally {
573
+ setSearchLoading(false);
574
+ }
575
+ }, 300); // 300ms debounce
576
+ }, [tableName, searchFields, where, orderBy, limit, offset]);
577
+
578
+ /**
579
+ * Fetch multiple records with support for filtering, sorting, and pagination.
580
+ *
581
+ * @param params - Query parameters for filtering and ordering records
582
+ * @returns A promise with the fetched data or error
583
+ *
584
+ * @example
585
+ * // Get all active users
586
+ * const result = await users.findMany({
587
+ * where: { active: true }
588
+ * });
589
+ *
590
+ * @example
591
+ * // Get 10 most recent posts with pagination
592
+ * const page1 = await posts.findMany({
593
+ * orderBy: { created_at: 'desc' },
594
+ * take: 10,
595
+ * skip: 0
596
+ * });
597
+ *
598
+ * const page2 = await posts.findMany({
599
+ * orderBy: { created_at: 'desc' },
600
+ * take: 10,
601
+ * skip: 10
602
+ * });
603
+ */
604
+ const findMany = useCallback(async (params?: {
605
+ where?: TWhereInput;
606
+ orderBy?: TOrderByInput;
607
+ take?: number;
608
+ skip?: number;
609
+ }): ModelResult<TWithRelations[]> => {
610
+ try {
611
+ setLoading(true);
612
+ setError(null);
613
+
614
+ let query = supabase.from(tableName).select('*');
615
+
616
+ // Apply where conditions if provided
617
+ if (params?.where) {
618
+ query = applyFilter(query, params.where);
619
+ }
620
+
621
+ // Apply order by if provided
622
+ if (params?.orderBy) {
623
+ query = applyOrderBy(query, params.orderBy, hasCreatedAt);
624
+ } else if (hasCreatedAt) {
625
+ // Default ordering if available
626
+ // @ts-ignore: Supabase typing issue
627
+ query = query.order('created_at', { ascending: false });
628
+ }
629
+
630
+ // Apply limit if provided
631
+ if (params?.take) {
632
+ query = query.limit(params.take);
633
+ }
634
+
635
+ // Apply offset if provided (for pagination)
636
+ if (params?.skip) {
637
+ query = query.range(params.skip, params.skip + (params.take || 10) - 1);
638
+ }
639
+
640
+ const { data, error } = await query;
641
+
642
+ if (error) throw error;
643
+
644
+ const typedData = (data || []) as TWithRelations[];
645
+
646
+ // Only update data if not currently searching
647
+ if (!isSearchingRef.current) {
648
+ setData(typedData);
649
+ }
650
+
651
+ return { data: typedData, error: null };
652
+ } catch (err: any) {
653
+ console.error('Error finding records:', err);
654
+ setError(err);
655
+ return { data: null, error: err };
656
+ } finally {
657
+ setLoading(false);
658
+ }
659
+ }, []);
660
+
661
+ /**
662
+ * Find a single record by its unique identifier (usually ID).
663
+ *
664
+ * @param where - The unique identifier to find the record by
665
+ * @returns A promise with the found record or error
666
+ *
667
+ * @example
668
+ * // Find user by ID
669
+ * const result = await users.findUnique({ id: "123" });
670
+ * if (result.data) {
671
+ * console.log("Found user:", result.data.name);
672
+ * }
673
+ */
674
+ const findUnique = useCallback(async (
675
+ where: TWhereUniqueInput
676
+ ): ModelResult<TWithRelations> => {
677
+ try {
678
+ setLoading(true);
679
+ setError(null);
680
+
681
+ // Find the primary field (usually 'id')
682
+ // @ts-ignore: Supabase typing issue
683
+ const primaryKey = Object.keys(where)[0];
684
+ if (!primaryKey) {
685
+ throw new Error('A unique identifier is required');
686
+ }
687
+
688
+ const value = where[primaryKey as keyof typeof where];
689
+ if (value === undefined) {
690
+ throw new Error('A unique identifier is required');
691
+ }
692
+
693
+ const { data, error } = await supabase
694
+ .from(tableName)
695
+ .select('*')
696
+ .eq(primaryKey, value)
697
+ .maybeSingle();
698
+
699
+ if (error) throw error;
700
+
701
+ return { data: data as TWithRelations, error: null };
702
+ } catch (err: any) {
703
+ console.error('Error finding unique record:', err);
704
+ setError(err);
705
+ return { data: null, error: err };
706
+ } finally {
707
+ setLoading(false);
708
+ }
709
+ }, []);
710
+
711
+ // Set up realtime subscription for the list
712
+ useEffect(() => {
713
+ if (!realtime) return;
714
+
715
+ // Clean up previous subscription if it exists
716
+ if (channelRef.current) {
717
+ channelRef.current.unsubscribe();
718
+ channelRef.current = null;
719
+ }
720
+
721
+ const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
722
+
723
+ // Store the current filter and options for closure
724
+ const currentFilter = computedFilter;
725
+ const currentWhere = where;
726
+ const currentOrderBy = orderBy;
727
+ const currentLimit = limit;
728
+ const currentOffset = offset;
729
+
730
+ console.log(\`Setting up subscription for \${tableName} with filter: \${currentFilter}\`);
731
+
732
+ const channel = supabase
733
+ .channel(channelId)
734
+ .on(
735
+ 'postgres_changes',
736
+ {
737
+ event: '*',
738
+ schema: 'public',
739
+ table: tableName,
740
+ filter: currentFilter,
741
+ },
742
+ (payload) => {
743
+ console.log(\`Received \${payload.eventType} event for \${tableName}\`, payload);
744
+
745
+ // Skip realtime updates when search is active
746
+ if (isSearchingRef.current) return;
747
+
748
+ if (payload.eventType === 'INSERT') {
749
+ // Process insert event
750
+ setData((prev) => {
751
+ try {
752
+ const newRecord = payload.new as TWithRelations;
753
+ console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
754
+
755
+ // Check if this record matches our filter if we have one
756
+ if (currentWhere) {
757
+ let matchesFilter = true;
758
+
759
+ // Check each filter condition
760
+ for (const [key, value] of Object.entries(currentWhere)) {
761
+ if (typeof value === 'object' && value !== null) {
762
+ // Complex filter - this is handled by Supabase, assume it matches
763
+ } else if (newRecord[key as keyof typeof newRecord] !== value) {
764
+ matchesFilter = false;
765
+ console.log(\`Filter mismatch on \${key}\`, { expected: value, actual: newRecord[key as keyof typeof newRecord] });
766
+ break;
767
+ }
768
+ }
769
+
770
+ if (!matchesFilter) {
771
+ console.log('New record does not match filter criteria, skipping');
772
+ return prev;
773
+ }
774
+ }
775
+
776
+ // Check if record already exists (avoid duplicates)
777
+ const exists = prev.some(item =>
778
+ // @ts-ignore: Supabase typing issue
779
+ 'id' in item && 'id' in newRecord && item.id === newRecord.id
780
+ );
781
+
782
+ if (exists) {
783
+ console.log('Record already exists, skipping insertion');
784
+ return prev;
785
+ }
786
+
787
+ // Add the new record to the data
788
+ let newData = [newRecord, ...prev];
789
+
790
+ // Apply ordering if specified
791
+ if (currentOrderBy) {
792
+ const [orderField, direction] = Object.entries(currentOrderBy)[0] || [];
793
+ if (orderField) {
794
+ newData = [...newData].sort((a, b) => {
795
+ const aValue = a[orderField as keyof typeof a] ?? '';
796
+ const bValue = b[orderField as keyof typeof b] ?? '';
797
+
798
+ if (direction === 'asc') {
799
+ return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
800
+ } else {
801
+ return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
802
+ }
803
+ });
804
+ }
805
+ }
806
+
807
+ // Apply limit if specified
808
+ if (currentLimit && currentLimit > 0) {
809
+ newData = newData.slice(0, currentLimit);
810
+ }
811
+
812
+ return newData;
813
+ } catch (error) {
814
+ console.error('Error processing INSERT event:', error);
815
+ return prev;
816
+ }
817
+ });
818
+ } else if (payload.eventType === 'UPDATE') {
819
+ // Process update event
820
+ setData((prev) => {
821
+ // Skip if search is active
822
+ if (isSearchingRef.current) {
823
+ return prev;
824
+ }
825
+
826
+ return prev.map((item) =>
827
+ // @ts-ignore: Supabase typing issue
828
+ 'id' in item && 'id' in payload.new && item.id === payload.new.id
829
+ ? (payload.new as TWithRelations)
830
+ : item
831
+ );
832
+ });
833
+ } else if (payload.eventType === 'DELETE') {
834
+ // Process delete event
835
+ setData((prev) => {
836
+ // Skip if search is active
837
+ if (isSearchingRef.current) {
838
+ return prev;
839
+ }
840
+
841
+ // Save the current size before filtering
842
+ const currentSize = prev.length;
843
+
844
+ // Filter out the deleted item
845
+ const filteredData = prev.filter((item) => {
846
+ // @ts-ignore: Supabase typing issue
847
+ return !('id' in item && 'id' in payload.old && item.id === payload.old.id);
848
+ });
849
+
850
+ // If we need to maintain the size with a limit
851
+ if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) {
852
+ console.log(\`Record deleted with limit \${currentLimit}, will fetch additional record to maintain size\`);
853
+
854
+ // Use setTimeout to ensure this state update completes first
855
+ setTimeout(() => {
856
+ findMany({
857
+ where: currentWhere,
858
+ orderBy: currentOrderBy,
859
+ take: currentLimit,
860
+ skip: currentOffset
861
+ });
862
+ }, 0);
863
+ }
864
+
865
+ return filteredData;
866
+ });
867
+ }
868
+ }
869
+ )
870
+ .subscribe((status) => {
871
+ console.log(\`Subscription status for \${tableName}\`, status);
872
+ });
873
+
874
+ // Store the channel ref
875
+ channelRef.current = channel;
876
+
877
+ return () => {
878
+ console.log(\`Unsubscribing from \${channelId}\`);
879
+ if (channelRef.current) {
880
+ channelRef.current.unsubscribe();
881
+ channelRef.current = null;
882
+ }
883
+
884
+ if (searchTimeoutRef.current) {
885
+ clearTimeout(searchTimeoutRef.current);
886
+ searchTimeoutRef.current = null;
887
+ }
888
+ };
889
+ }, [realtime, channelName, computedFilter]);
890
+
891
+ // Create a memoized options object to prevent unnecessary re-renders
892
+ const optionsRef = useRef({ where, orderBy, limit, offset });
893
+
894
+ // Compare current options with previous options
895
+ const optionsChanged = useCallback(() => {
896
+ // Create stable string representations for deep comparison
897
+ const whereStr = where ? JSON.stringify(where) : '';
898
+ const orderByStr = orderBy ? JSON.stringify(orderBy) : '';
899
+ const prevWhereStr = optionsRef.current.where ? JSON.stringify(optionsRef.current.where) : '';
900
+ const prevOrderByStr = optionsRef.current.orderBy ? JSON.stringify(optionsRef.current.orderBy) : '';
901
+
902
+ // Compare the stable representations
903
+ const hasChanged =
904
+ whereStr !== prevWhereStr ||
905
+ orderByStr !== prevOrderByStr ||
906
+ limit !== optionsRef.current.limit ||
907
+ offset !== optionsRef.current.offset;
908
+
909
+ if (hasChanged) {
910
+ // Update the ref with the new values
911
+ optionsRef.current = { where, orderBy, limit, offset };
912
+ return true;
913
+ }
914
+
915
+ return false;
916
+ }, [where, orderBy, limit, offset]);
917
+
918
+ // Load initial data based on options
919
+ useEffect(() => {
920
+ // Skip if search is active
921
+ if (isSearchingRef.current) return;
922
+
923
+ // Skip if we've already loaded or if no filter criteria are provided
924
+ if (initialLoadRef.current) {
925
+ // Only reload if options have changed significantly
926
+ if (optionsChanged()) {
927
+ console.log(\`Options changed for \${tableName}, reloading data\`);
928
+ findMany({
929
+ where,
930
+ orderBy,
931
+ take: limit,
932
+ skip: offset
933
+ });
934
+ }
935
+ return;
936
+ }
937
+
938
+ // Initial load
939
+ initialLoadRef.current = true;
940
+ findMany({
941
+ where,
942
+ orderBy,
943
+ take: limit,
944
+ skip: offset
945
+ });
946
+ }, [findMany, where, orderBy, limit, offset, optionsChanged]);
947
+
948
+ /**
949
+ * Create a new record with the provided data.
950
+ * Default values from the schema will be applied if not provided.
951
+ *
952
+ * @param data - The data to create the record with
953
+ * @returns A promise with the created record or error
954
+ *
955
+ * @example
956
+ * // Create a new user
957
+ * const result = await users.create({
958
+ * name: "John Doe",
959
+ * email: "john@example.com"
960
+ * });
961
+ *
962
+ * @example
963
+ * // Create with custom ID (overriding default)
964
+ * const result = await users.create({
965
+ * id: "custom-id-123",
966
+ * name: "John Doe"
967
+ * });
968
+ */
969
+ const create = useCallback(async (
970
+ data: TCreateInput
971
+ ): ModelResult<TWithRelations> => {
972
+ try {
973
+ setLoading(true);
974
+ setError(null);
975
+
976
+ const now = new Date().toISOString();
977
+
978
+ // Apply default values from schema
979
+ const appliedDefaults: Record<string, any> = {};
980
+
981
+ // Apply all default values that aren't already in the data
982
+ for (const [field, defaultValue] of Object.entries(defaultValues)) {
983
+ // @ts-ignore: Supabase typing issue
984
+ if (!(field in data)) {
985
+ // Parse the default value based on its type
986
+ if (defaultValue.includes('now()') || defaultValue.includes('now')) {
987
+ appliedDefaults[field] = now;
988
+ } else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
989
+ appliedDefaults[field] = crypto.randomUUID();
990
+ } else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
991
+ // Simple cuid-like implementation for client-side
992
+ appliedDefaults[field] = 'c' + Math.random().toString(36).substring(2, 15);
993
+ } else if (defaultValue.includes('true')) {
994
+ appliedDefaults[field] = true;
995
+ } else if (defaultValue.includes('false')) {
996
+ appliedDefaults[field] = false;
997
+ } else if (!isNaN(Number(defaultValue))) {
998
+ // If it's a number
999
+ appliedDefaults[field] = Number(defaultValue);
1000
+ } else {
1001
+ // String or other value, remove quotes if present
1002
+ const strValue = defaultValue.replace(/^["'](.*)["']$/, '$1');
1003
+ appliedDefaults[field] = strValue;
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ const itemWithDefaults = {
1009
+ ...appliedDefaults, // Apply schema defaults first
1010
+ ...data, // Then user data (overrides defaults)
1011
+ ...(hasCreatedAt ? { created_at: now } : {}), // Always set timestamps
1012
+ ...(hasUpdatedAt ? { updated_at: now } : {})
1013
+ };
1014
+
1015
+ const { data: result, error } = await supabase
1016
+ .from(tableName)
1017
+ .insert([itemWithDefaults])
1018
+ .select();
1019
+
1020
+ if (error) throw error;
1021
+
1022
+ // Return created record
1023
+ return { data: result?.[0] as TWithRelations, error: null };
1024
+ } catch (err: any) {
1025
+ console.error('Error creating record:', err);
1026
+ setError(err);
1027
+ return { data: null, error: err };
1028
+ } finally {
1029
+ setLoading(false);
1030
+ }
1031
+ }, []);
1032
+
1033
+ /**
1034
+ * Update an existing record identified by a unique identifier.
1035
+ *
1036
+ * @param params - Object containing the identifier and update data
1037
+ * @returns A promise with the updated record or error
1038
+ *
1039
+ * @example
1040
+ * // Update a user's name
1041
+ * const result = await users.update({
1042
+ * where: { id: "123" },
1043
+ * data: { name: "New Name" }
1044
+ * });
1045
+ *
1046
+ * @example
1047
+ * // Update multiple fields
1048
+ * const result = await users.update({
1049
+ * where: { id: "123" },
1050
+ * data: {
1051
+ * name: "New Name",
1052
+ * active: false
1053
+ * }
1054
+ * });
1055
+ */
1056
+ const update = useCallback(async (params: {
1057
+ where: TWhereUniqueInput;
1058
+ data: TUpdateInput;
1059
+ }): ModelResult<TWithRelations> => {
1060
+ try {
1061
+ setLoading(true);
1062
+ setError(null);
1063
+
1064
+ // Find the primary field (usually 'id')
1065
+ // @ts-ignore: Supabase typing issue
1066
+ const primaryKey = Object.keys(params.where)[0];
1067
+ if (!primaryKey) {
1068
+ throw new Error('A unique identifier is required');
1069
+ }
1070
+
1071
+ const value = params.where[primaryKey as keyof typeof params.where];
1072
+ if (value === undefined) {
1073
+ throw new Error('A unique identifier is required');
1074
+ }
1075
+
1076
+ const now = new Date().toISOString();
1077
+
1078
+ // We do not apply default values for updates
1079
+ // Default values are only for creation, not for updates,
1080
+ // as existing records already have these values set
1081
+
1082
+ const itemWithDefaults = {
1083
+ ...params.data,
1084
+ ...(hasUpdatedAt ? { updated_at: now } : {})
1085
+ };
1086
+
1087
+ const { data, error } = await supabase
1088
+ .from(tableName)
1089
+ .update(itemWithDefaults)
1090
+ .eq(primaryKey, value)
1091
+ .select();
1092
+
1093
+ if (error) throw error;
1094
+
1095
+ // Return updated record
1096
+ return { data: data?.[0] as TWithRelations, error: null };
1097
+ } catch (err: any) {
1098
+ console.error('Error updating record:', err);
1099
+ setError(err);
1100
+ return { data: null, error: err };
1101
+ } finally {
1102
+ setLoading(false);
1103
+ }
1104
+ }, []);
1105
+
1106
+ /**
1107
+ * Delete a record by its unique identifier.
1108
+ *
1109
+ * @param where - The unique identifier to delete the record by
1110
+ * @returns A promise with the deleted record or error
1111
+ *
1112
+ * @example
1113
+ * // Delete a user by ID
1114
+ * const result = await users.delete({ id: "123" });
1115
+ * if (result.data) {
1116
+ * console.log("Deleted user:", result.data.name);
1117
+ * }
1118
+ */
1119
+ const deleteRecord = useCallback(async (
1120
+ where: TWhereUniqueInput
1121
+ ): ModelResult<TWithRelations> => {
1122
+ try {
1123
+ setLoading(true);
1124
+ setError(null);
1125
+
1126
+ // Find the primary field (usually 'id')
1127
+ // @ts-ignore: Supabase typing issue
1128
+ const primaryKey = Object.keys(where)[0];
1129
+ if (!primaryKey) {
1130
+ throw new Error('A unique identifier is required');
1131
+ }
1132
+
1133
+ const value = where[primaryKey as keyof typeof where];
1134
+ if (value === undefined) {
1135
+ throw new Error('A unique identifier is required');
1136
+ }
1137
+
1138
+ // First fetch the record to return it
1139
+ const { data: recordToDelete } = await supabase
1140
+ .from(tableName)
1141
+ .select('*')
1142
+ .eq(primaryKey, value)
1143
+ .maybeSingle();
1144
+
1145
+ if (!recordToDelete) {
1146
+ throw new Error('Record not found');
1147
+ }
1148
+
1149
+ // Then delete it
1150
+ const { error } = await supabase
1151
+ .from(tableName)
1152
+ .delete()
1153
+ .eq(primaryKey, value);
1154
+
1155
+ if (error) throw error;
1156
+
1157
+ // Return the deleted record
1158
+ return { data: recordToDelete as TWithRelations, error: null };
1159
+ } catch (err: any) {
1160
+ console.error('Error deleting record:', err);
1161
+ setError(err);
1162
+ return { data: null, error: err };
1163
+ } finally {
1164
+ setLoading(false);
1165
+ }
1166
+ }, []);
1167
+
1168
+ /**
1169
+ * Delete multiple records matching the filter criteria.
1170
+ *
1171
+ * @param params - Query parameters for filtering records to delete
1172
+ * @returns A promise with the count of deleted records or error
1173
+ *
1174
+ * @example
1175
+ * // Delete all inactive users
1176
+ * const result = await users.deleteMany({
1177
+ * where: { active: false }
1178
+ * });
1179
+ * console.log(\`Deleted \${result.count} inactive users\`);
1180
+ *
1181
+ * @example
1182
+ * // Delete all records (use with caution!)
1183
+ * const result = await users.deleteMany();
1184
+ */
1185
+ const deleteMany = useCallback(async (params?: {
1186
+ where?: TWhereInput;
1187
+ }): Promise<{ count: number; error: Error | null }> => {
1188
+ try {
1189
+ setLoading(true);
1190
+ setError(null);
1191
+
1192
+ // First, get the records that will be deleted to count them
1193
+ let query = supabase.from(tableName).select('*');
1194
+
1195
+ // Apply where conditions if provided
1196
+ if (params?.where) {
1197
+ query = applyFilter(query, params.where);
1198
+ }
1199
+
1200
+ // Get records that will be deleted
1201
+ const { data: recordsToDelete, error: fetchError } = await query;
1202
+
1203
+ if (fetchError) throw fetchError;
1204
+
1205
+ if (!recordsToDelete?.length) {
1206
+ return { count: 0, error: null };
1207
+ }
1208
+
1209
+ // Build the delete query
1210
+ let deleteQuery = supabase.from(tableName).delete();
1211
+
1212
+ // Apply the same filter to the delete operation
1213
+ if (params?.where) {
1214
+ // @ts-ignore: Supabase typing issue
1215
+ deleteQuery = applyFilter(deleteQuery, params.where);
1216
+ }
1217
+
1218
+ // Perform the delete
1219
+ const { error: deleteError } = await deleteQuery;
1220
+
1221
+ if (deleteError) throw deleteError;
1222
+
1223
+ // Return the count of deleted records
1224
+ return { count: recordsToDelete.length, error: null };
1225
+ } catch (err: any) {
1226
+ console.error('Error deleting multiple records:', err);
1227
+ setError(err);
1228
+ return { count: 0, error: err };
1229
+ } finally {
1230
+ setLoading(false);
1231
+ }
1232
+ }, []);
1233
+
1234
+ /**
1235
+ * Find the first record matching the filter criteria.
1236
+ *
1237
+ * @param params - Query parameters for filtering and ordering
1238
+ * @returns A promise with the first matching record or error
1239
+ *
1240
+ * @example
1241
+ * // Find the first admin user
1242
+ * const result = await users.findFirst({
1243
+ * where: { role: 'admin' }
1244
+ * });
1245
+ *
1246
+ * @example
1247
+ * // Find the oldest post
1248
+ * const result = await posts.findFirst({
1249
+ * orderBy: { created_at: 'asc' }
1250
+ * });
1251
+ */
1252
+ const findFirst = useCallback(async (params?: {
1253
+ where?: TWhereInput;
1254
+ orderBy?: TOrderByInput;
1255
+ }): ModelResult<TWithRelations> => {
1256
+ try {
1257
+ const result = await findMany({
1258
+ ...params,
1259
+ take: 1
1260
+ });
1261
+
1262
+ if (result.error) return { data: null, error: result.error };
1263
+ if (!result.data.length) return { data: null, error: new Error('No records found') };
1264
+
1265
+ // @ts-ignore: Supabase typing issue
1266
+ return { data: result.data[0], error: null };
1267
+ } catch (err: any) {
1268
+ console.error('Error finding first record:', err);
1269
+ return { data: null, error: err };
1270
+ }
1271
+ }, [findMany]);
1272
+
1273
+ /**
1274
+ * Create a record if it doesn't exist, or update it if it does.
1275
+ *
1276
+ * @param params - Object containing the identifier, update data, and create data
1277
+ * @returns A promise with the created or updated record or error
1278
+ *
1279
+ * @example
1280
+ * // Upsert a user by ID
1281
+ * const result = await users.upsert({
1282
+ * where: { id: "123" },
1283
+ * update: { lastLogin: new Date().toISOString() },
1284
+ * create: {
1285
+ * id: "123",
1286
+ * name: "John Doe",
1287
+ * email: "john@example.com",
1288
+ * lastLogin: new Date().toISOString()
1289
+ * }
1290
+ * });
1291
+ */
1292
+ const upsert = useCallback(async (params: {
1293
+ where: TWhereUniqueInput;
1294
+ update: TUpdateInput;
1295
+ create: TCreateInput;
1296
+ }): ModelResult<TWithRelations> => {
1297
+ try {
1298
+ // Check if record exists
1299
+ const { data: existing } = await findUnique(params.where);
1300
+
1301
+ // Update if exists, otherwise create
1302
+ if (existing) {
1303
+ return update({ where: params.where, data: params.update });
1304
+ } else {
1305
+ return create(params.create);
1306
+ }
1307
+ } catch (err: any) {
1308
+ console.error('Error upserting record:', err);
1309
+ return { data: null, error: err };
1310
+ }
1311
+ }, [findUnique, update, create]);
1312
+
1313
+ /**
1314
+ * Count the number of records matching the filter criteria.
1315
+ *
1316
+ * @param params - Query parameters for filtering
1317
+ * @returns A promise with the count of matching records
1318
+ *
1319
+ * @example
1320
+ * // Count all users
1321
+ * const count = await users.count();
1322
+ *
1323
+ * @example
1324
+ * // Count active users
1325
+ * const activeCount = await users.count({
1326
+ * where: { active: true }
1327
+ * });
1328
+ */
1329
+ const count = useCallback(async (params?: {
1330
+ where?: TWhereInput;
1331
+ }): Promise<number> => {
1332
+ try {
1333
+ let query = supabase.from(tableName).select('*', { count: 'exact', head: true });
1334
+
1335
+ // Use provided where filter, or fall back to the hook's original where filter
1336
+ const effectiveWhere = params?.where ?? where;
1337
+
1338
+ if (effectiveWhere) {
1339
+ query = applyFilter(query, effectiveWhere);
1340
+ }
1341
+
1342
+ const { count, error } = await query;
1343
+
1344
+ if (error) throw error;
1345
+
1346
+ return count || 0;
1347
+ } catch (err) {
1348
+ console.error('Error counting records:', err);
1349
+ return 0;
1350
+ }
1351
+ }, [where]);
1352
+
1353
+ /**
1354
+ * Manually refresh the data with current filter settings.
1355
+ * Useful after external operations or when realtime is disabled.
1356
+ *
1357
+ * @param params - Optional override parameters for this specific refresh
1358
+ * @returns A promise with the refreshed data or error
1359
+ *
1360
+ * @example
1361
+ * // Refresh with current filter settings
1362
+ * await users.refresh();
1363
+ *
1364
+ * @example
1365
+ * // Refresh with different filters for this call only
1366
+ * await users.refresh({
1367
+ * where: { active: true },
1368
+ * orderBy: { name: 'asc' }
1369
+ * });
1370
+ */
1371
+ const refresh = useCallback((params?: {
1372
+ where?: TWhereInput;
1373
+ orderBy?: TOrderByInput;
1374
+ take?: number;
1375
+ skip?: number;
1376
+ }) => {
1377
+ // If search is active, refresh search results
1378
+ if (isSearchingRef.current && searchQueries.length > 0) {
1379
+ executeSearch(searchQueries);
1380
+ return Promise.resolve({ data: data, error: null });
1381
+ }
1382
+
1383
+ // Otherwise, refresh normal data using original params if not explicitly overridden
1384
+ return findMany({
1385
+ where: params?.where ?? where,
1386
+ orderBy: params?.orderBy ?? orderBy,
1387
+ take: params?.take ?? limit,
1388
+ skip: params?.skip ?? offset
1389
+ });
1390
+ }, [findMany, data, searchQueries, where, orderBy, limit, offset]);
1391
+
1392
+ // Construct final hook API with or without search
1393
+ const api = {
1394
+ // State
1395
+ data,
1396
+ error,
1397
+ loading,
1398
+
1399
+ // Finder methods
1400
+ findUnique,
1401
+ findMany,
1402
+ findFirst,
1403
+
1404
+ // Mutation methods
1405
+ create,
1406
+ update,
1407
+ delete: deleteRecord,
1408
+ deleteMany,
1409
+ upsert,
1410
+
1411
+ // Utilities
1412
+ count,
1413
+
1414
+ // Manual refresh
1415
+ refresh
1416
+ };
1417
+
1418
+ // Add search object if searchable fields are present
1419
+ return searchFields.length > 0
1420
+ ? {
1421
+ ...api,
1422
+ search
1423
+ }
1424
+ : api;
1425
+ };
1426
+ }`;
1427
+ const outputPath = path_1.default.join(config_1.OUTPUT_DIR, 'core.ts');
1428
+ fs_1.default.writeFileSync(outputPath, coreContent);
1429
+ console.log(`Updated core hook factory with improved refresh and count functions at ${outputPath}`);
1430
+ }