suparisma 1.1.2 → 1.2.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.
@@ -23,9 +23,13 @@ import { supabase } from './supabase-client';
23
23
  * @example
24
24
  * // Search for users with names containing "john"
25
25
  * const query = { field: "name", value: "john" };
26
+ *
27
+ * @example
28
+ * // Search across multiple fields
29
+ * const query = { field: "multi", value: "john" };
26
30
  */
27
31
  export type SearchQuery = {
28
- /** The field name to search in */
32
+ /** The field name to search in, or "multi" for multi-field search */
29
33
  field: string;
30
34
  /** The search term/value to look for */
31
35
  value: string;
@@ -34,6 +38,51 @@ export type SearchQuery = {
34
38
  // Define type for Supabase query builder
35
39
  export type SupabaseQueryBuilder = ReturnType<ReturnType<typeof supabase.from>['select']>;
36
40
 
41
+ /**
42
+ * Utility function to escape regex special characters for safe RegExp usage
43
+ * Prevents "Invalid regular expression" errors when search terms contain special characters
44
+ */
45
+ export function escapeRegexCharacters(str: string): string {
46
+ // Escape all special regex characters: ( ) [ ] { } + * ? ^ $ | . \\
47
+ return str.replace(/[()\\[\\]{}+*?^$|.\\\\]/g, '\\\\\\\\$&');
48
+ }
49
+
50
+ /**
51
+ * Generate a UUID v4, with fallback for environments without crypto.randomUUID()
52
+ * Works in: browsers, Node.js, and React Native (with react-native-get-random-values polyfill)
53
+ *
54
+ * For React Native, ensure you have installed and imported the polyfill:
55
+ * - pnpm install react-native-get-random-values
56
+ * - Import at app entry point: import 'react-native-get-random-values';
57
+ */
58
+ export function generateUUID(): string {
59
+ // Try native crypto.randomUUID() first (modern browsers & Node.js 16.7+)
60
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
61
+ return crypto.randomUUID();
62
+ }
63
+
64
+ // Fallback using crypto.getRandomValues() (works with react-native-get-random-values polyfill)
65
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
66
+ const bytes = new Uint8Array(16);
67
+ crypto.getRandomValues(bytes);
68
+
69
+ // Set version (4) and variant (RFC 4122)
70
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
71
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
72
+
73
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
74
+ return \`\${hex.slice(0, 8)}-\${hex.slice(8, 12)}-\${hex.slice(12, 16)}-\${hex.slice(16, 20)}-\${hex.slice(20)}\`;
75
+ }
76
+
77
+ // Last resort fallback using Math.random() (not cryptographically secure)
78
+ console.warn('[Suparisma] crypto API not available, using Math.random() fallback for UUID generation');
79
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
80
+ const r = (Math.random() * 16) | 0;
81
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
82
+ return v.toString(16);
83
+ });
84
+ }
85
+
37
86
  /**
38
87
  * Advanced filter operators for complex queries
39
88
  * @example
@@ -162,10 +211,22 @@ export type ModelResult<T> = Promise<{
162
211
  * users.search.addQuery({ field: "name", value: "john" });
163
212
  *
164
213
  * @example
214
+ * // Search across multiple fields
215
+ * users.search.searchMultiField("john doe");
216
+ *
217
+ * @example
165
218
  * // Check if search is loading
166
219
  * if (users.search.loading) {
167
220
  * return <div>Searching...</div>;
168
221
  * }
222
+ *
223
+ * @example
224
+ * // Get current search terms for highlighting
225
+ * const searchTerms = users.search.getCurrentSearchTerms();
226
+ *
227
+ * @example
228
+ * // Safely escape regex characters
229
+ * const escaped = users.search.escapeRegex("user@example.com");
169
230
  */
170
231
  export type SearchState = {
171
232
  /** Current active search queries */
@@ -180,6 +241,14 @@ export type SearchState = {
180
241
  removeQuery: (field: string) => void;
181
242
  /** Clear all search queries and return to normal data fetching */
182
243
  clearQueries: () => void;
244
+ /** Search across multiple fields (convenience method) */
245
+ searchMultiField: (value: string) => void;
246
+ /** Search in a specific field (convenience method) */
247
+ searchField: (field: string, value: string) => void;
248
+ /** Get current search terms for custom highlighting */
249
+ getCurrentSearchTerms: () => string[];
250
+ /** Safely escape regex special characters */
251
+ escapeRegex: (text: string) => string;
183
252
  };
184
253
 
185
254
  /**
@@ -897,7 +966,39 @@ export function createSuparismaHook<
897
966
  setSearchQueries([]);
898
967
  isSearchingRef.current = false;
899
968
  findMany({ where, orderBy, take: limit, skip: offset });
900
- }, [where, orderBy, limit, offset])
969
+ }, [where, orderBy, limit, offset]),
970
+
971
+ // Search across multiple fields (convenience method)
972
+ searchMultiField: useCallback((value: string) => {
973
+ if (searchFields.length <= 1) {
974
+ console.warn('Multi-field search requires at least 2 searchable fields');
975
+ return;
976
+ }
977
+
978
+ setSearchQueries([{ field: 'multi', value }]);
979
+ executeSearch([{ field: 'multi', value }]);
980
+ }, [searchFields.length]),
981
+
982
+ // Search in a specific field (convenience method)
983
+ searchField: useCallback((field: string, value: string) => {
984
+ if (!searchFields.includes(field)) {
985
+ console.warn(\`Field "\${field}" is not searchable. Available fields: \${searchFields.join(', ')}\`);
986
+ return;
987
+ }
988
+
989
+ setSearchQueries([{ field, value }]);
990
+ executeSearch([{ field, value }]);
991
+ }, [searchFields]),
992
+
993
+ // Get current search terms for custom highlighting
994
+ getCurrentSearchTerms: useCallback(() => {
995
+ return searchQueries.map(q => q.value.trim());
996
+ }, [searchQueries]),
997
+
998
+ // Safely escape regex special characters
999
+ escapeRegex: useCallback((text: string) => {
1000
+ return escapeRegexCharacters(text);
1001
+ }, [])
901
1002
  };
902
1003
 
903
1004
  // Execute search based on queries
@@ -920,13 +1021,52 @@ export function createSuparismaHook<
920
1021
  try {
921
1022
  let results: TWithRelations[] = [];
922
1023
 
1024
+ // Validate search queries
1025
+ const validQueries = queries.filter(query => {
1026
+ if (!query.field || !query.value) {
1027
+ console.warn('Invalid search query - missing field or value:', query);
1028
+ return false;
1029
+ }
1030
+ // Allow "multi" as a special field for multi-field search
1031
+ if (query.field === 'multi' && searchFields.length > 1) {
1032
+ return true;
1033
+ }
1034
+ if (!searchFields.includes(query.field)) {
1035
+ console.warn(\`Field "\${query.field}" is not searchable. Available fields: \${searchFields.join(', ')}, or "multi" for multi-field search\`);
1036
+ return false;
1037
+ }
1038
+ return true;
1039
+ });
1040
+
1041
+ if (validQueries.length === 0) {
1042
+ console.log('No valid search queries found');
1043
+ setData([]);
1044
+ setCount(0);
1045
+ return;
1046
+ }
1047
+
923
1048
  // Execute RPC function for each query using Promise.all
924
- const searchPromises = queries.map(query => {
925
- // Build function name: search_tablename_by_fieldname_prefix
926
- const functionName = \`search_\${tableName}_by_\${query.field}_prefix\`;
1049
+ const searchPromises = validQueries.map(query => {
1050
+ // Build function name based on field type
1051
+ const functionName = query.field === 'multi'
1052
+ ? \`search_\${tableName.toLowerCase()}_multi_field\`
1053
+ : \`search_\${tableName.toLowerCase()}_by_\${query.field.toLowerCase()}_prefix\`;
1054
+
1055
+ console.log(\`🔍 Executing search: \${functionName}(search_prefix: "\${query.value.trim()}")\`);
927
1056
 
928
- // Call RPC function
929
- return supabase.rpc(functionName, { prefix: query.value.trim() });
1057
+ // Call RPC function with proper error handling
1058
+ return Promise.resolve(supabase.rpc(functionName, { search_prefix: query.value.trim() }))
1059
+ .then((result: any) => ({
1060
+ ...result,
1061
+ queryField: query.field,
1062
+ queryValue: query.value
1063
+ }))
1064
+ .catch((error: any) => ({
1065
+ data: null,
1066
+ error: error,
1067
+ queryField: query.field,
1068
+ queryValue: query.value
1069
+ }));
930
1070
  });
931
1071
 
932
1072
  // Execute all search queries in parallel
@@ -934,82 +1074,88 @@ export function createSuparismaHook<
934
1074
 
935
1075
  // Combine and deduplicate results
936
1076
  const allResults: Record<string, TWithRelations> = {};
1077
+ let hasErrors = false;
937
1078
 
938
1079
  // Process each search result
939
- searchResults.forEach((result, index) => {
1080
+ searchResults.forEach((result: any, index: number) => {
940
1081
  if (result.error) {
941
- console.error(\`Search error for \${queries[index]?.field}:\`, result.error);
1082
+ console.error(\`🔍 Search error for field "\${result.queryField}" with value "\${result.queryValue}":\`, result.error);
1083
+ hasErrors = true;
942
1084
  return;
943
1085
  }
944
1086
 
945
- if (result.data) {
1087
+ if (result.data && Array.isArray(result.data)) {
1088
+ console.log(\`🔍 Search results for "\${result.queryField}": \${result.data.length} items\`);
1089
+
946
1090
  // Add each result, using id as key to deduplicate
947
1091
  for (const item of result.data as TWithRelations[]) {
948
1092
  // @ts-ignore: Assume item has an id property
949
- if (item.id) {
1093
+ if (item && typeof item === 'object' && 'id' in item && item.id) {
950
1094
  // @ts-ignore: Add to results using id as key
951
1095
  allResults[item.id] = item;
952
1096
  }
953
1097
  }
1098
+ } else if (result.data) {
1099
+ console.warn(\`🔍 Unexpected search result format for "\${result.queryField}":\`, typeof result.data);
954
1100
  }
955
1101
  });
956
1102
 
957
1103
  // Convert back to array
958
1104
  results = Object.values(allResults);
1105
+ console.log(\`🔍 Combined search results: \${results.length} unique items\`);
959
1106
 
960
- // Apply any where conditions client-side
1107
+ // Apply any where conditions client-side (now using the proper filter function)
961
1108
  if (where) {
962
- results = results.filter((item) => {
963
- for (const [key, value] of Object.entries(where)) {
964
- if (typeof value === 'object' && value !== null) {
965
- // Skip complex filters for now
966
- continue;
967
- }
968
-
969
- if (item[key as keyof typeof item] !== value) {
970
- return false;
971
- }
972
- }
973
- return true;
974
- });
1109
+ const originalCount = results.length;
1110
+ results = results.filter((item) => matchesFilter(item, where));
1111
+ console.log(\`🔍 After applying where filter: \${results.length}/\${originalCount} items\`);
975
1112
  }
976
1113
 
977
1114
  // Set count directly for search results
978
1115
  setCount(results.length);
979
1116
 
980
- // Apply ordering if needed
1117
+ // Apply ordering if needed (using the proper compare function)
981
1118
  if (orderBy) {
982
- const orderEntries = Object.entries(orderBy);
983
- if (orderEntries.length > 0) {
984
- const [orderField, direction] = orderEntries[0] || [];
1119
+ const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];
985
1120
  results = [...results].sort((a, b) => {
986
- const aValue = a[orderField as keyof typeof a] ?? '';
987
- const bValue = b[orderField as keyof typeof b] ?? '';
1121
+ for (const orderByClause of orderByArray) {
1122
+ for (const [field, direction] of Object.entries(orderByClause)) {
1123
+ const aValue = a[field as keyof typeof a];
1124
+ const bValue = b[field as keyof typeof b];
988
1125
 
989
- if (direction === 'asc') {
990
- return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
991
- } else {
992
- return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
1126
+ if (aValue === bValue) continue;
1127
+
1128
+ return compareValues(aValue, bValue, direction as 'asc' | 'desc');
993
1129
  }
994
- });
995
- }
1130
+ }
1131
+ return 0;
1132
+ });
996
1133
  }
997
1134
 
998
1135
  // Apply pagination if needed
999
1136
  let paginatedResults = results;
1000
- if (limit && limit > 0) {
1001
- paginatedResults = results.slice(0, limit);
1002
- }
1003
-
1004
1137
  if (offset && offset > 0) {
1005
1138
  paginatedResults = paginatedResults.slice(offset);
1006
1139
  }
1007
1140
 
1141
+ if (limit && limit > 0) {
1142
+ paginatedResults = paginatedResults.slice(0, limit);
1143
+ }
1144
+
1145
+ console.log(\`🔍 Final search results: \${paginatedResults.length} items (total: \${results.length})\`);
1146
+
1008
1147
  // Update data with search results
1009
1148
  setData(paginatedResults);
1149
+
1150
+ // Show error if there were issues but still show partial results
1151
+ if (hasErrors && results.length === 0) {
1152
+ setError(new Error('Search failed - please check if search functions are properly configured'));
1153
+ }
1010
1154
  } catch (err) {
1011
- console.error('Search error:', err);
1155
+ console.error('🔍 Search error:', err);
1012
1156
  setError(err as Error);
1157
+ setData([]);
1158
+ setCount(0);
1013
1159
  } finally {
1014
1160
  setSearchLoading(false);
1015
1161
  }
@@ -1580,7 +1726,7 @@ export function createSuparismaHook<
1580
1726
  if (defaultValue.includes('now()') || defaultValue.includes('now')) {
1581
1727
  appliedDefaults[field] = now.toISOString(); // Database expects ISO string
1582
1728
  } else if (defaultValue.includes('uuid()') || defaultValue.includes('uuid')) {
1583
- appliedDefaults[field] = crypto.randomUUID();
1729
+ appliedDefaults[field] = generateUUID();
1584
1730
  } else if (defaultValue.includes('cuid()') || defaultValue.includes('cuid')) {
1585
1731
  // Simple cuid-like implementation for client-side
1586
1732
  appliedDefaults[field] = 'c' + Math.random().toString(36).substring(2, 15);
@@ -6,21 +6,83 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateSupabaseClientFile = generateSupabaseClientFile;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- const config_1 = require("../config"); // Ensure this is UTILS_DIR
9
+ const config_1 = require("../config");
10
+ /**
11
+ * Generate the Supabase client file based on the target platform.
12
+ * Supports both web (Next.js, etc.) and React Native/Expo.
13
+ */
10
14
  function generateSupabaseClientFile() {
11
- const supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
15
+ let supabaseClientContent;
16
+ if (config_1.PLATFORM === 'react-native') {
17
+ // React Native / Expo compatible client
18
+ supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
19
+ // Platform: React Native / Expo
20
+ //
21
+ // IMPORTANT: Before using Suparisma in React Native, ensure you have:
22
+ // 1. Installed required dependencies:
23
+ // pnpm install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
24
+ //
25
+ // 2. Added polyfills at your app's entry point (e.g., App.tsx or index.js):
26
+ // import 'react-native-url-polyfill/auto';
27
+ //
28
+ // 3. Set your Supabase credentials below or via environment variables
29
+
30
+ import AsyncStorage from '@react-native-async-storage/async-storage';
12
31
  import { createClient } from '@supabase/supabase-js';
13
32
 
14
- export const supabase = createClient(
15
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
16
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
17
- );
33
+ // Option 1: Set your Supabase credentials directly (for quick setup)
34
+ // const SUPABASE_URL = 'https://your-project.supabase.co';
35
+ // const SUPABASE_ANON_KEY = 'your-anon-key';
36
+
37
+ // Option 2: Use environment variables (recommended for production)
38
+ // With Expo, use expo-constants or babel-plugin-inline-dotenv
39
+ // With bare React Native, use react-native-dotenv
40
+ const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || '';
41
+ const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || '';
42
+
43
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
44
+ console.warn(
45
+ '[Suparisma] Supabase credentials not found. Please set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY ' +
46
+ 'in your environment variables, or update the credentials directly in this file.'
47
+ );
48
+ }
49
+
50
+ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
51
+ auth: {
52
+ storage: AsyncStorage,
53
+ autoRefreshToken: true,
54
+ persistSession: true,
55
+ detectSessionInUrl: false, // Important for React Native
56
+ },
57
+ });
18
58
  `;
59
+ }
60
+ else {
61
+ // Web platform (Next.js, Remix, etc.)
62
+ supabaseClientContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
63
+ // Platform: Web (Next.js, Remix, etc.)
64
+ import { createClient } from '@supabase/supabase-js';
65
+
66
+ // For Next.js, use NEXT_PUBLIC_ prefix
67
+ // For other frameworks, adjust the environment variable names as needed
68
+ const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || '';
69
+ const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || '';
70
+
71
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
72
+ console.warn(
73
+ '[Suparisma] Supabase credentials not found. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY ' +
74
+ '(or SUPABASE_URL and SUPABASE_ANON_KEY) in your environment variables.'
75
+ );
76
+ }
77
+
78
+ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
79
+ `;
80
+ }
19
81
  // Output to the UTILS_DIR
20
82
  const outputPath = path_1.default.join(config_1.UTILS_DIR, 'supabase-client.ts');
21
83
  if (!fs_1.default.existsSync(config_1.UTILS_DIR)) {
22
84
  fs_1.default.mkdirSync(config_1.UTILS_DIR, { recursive: true });
23
85
  }
24
86
  fs_1.default.writeFileSync(outputPath, supabaseClientContent);
25
- console.log(`🚀 Generated Supabase client file at: ${outputPath}`);
87
+ console.log(`🚀 Generated Supabase client file at: ${outputPath} (platform: ${config_1.PLATFORM})`);
26
88
  }
@@ -118,33 +118,16 @@ function generateModelTypesFile(model) {
118
118
  // Generate imports section for zod custom types
119
119
  let customImports = '';
120
120
  if (model.zodImports && model.zodImports.length > 0) {
121
- // Add zod import for type inference
122
- customImports = 'import { z } from \'zod\';\n';
123
- // Get the zod schemas file path from environment variable
124
- const zodSchemasPath = process.env.ZOD_SCHEMAS_FILE_PATH || '../commonTypes';
125
- // Add custom imports with environment variable path
126
- customImports += model.zodImports
127
- .map(zodImport => {
128
- // Extract the types from the original import statement
129
- const typeMatch = zodImport.importStatement.match(/import\s+{\s*([^}]+)\s*}\s+from/);
130
- if (typeMatch) {
131
- const types = typeMatch[1].trim();
132
- return `import { ${types} } from '${zodSchemasPath}'`;
133
- }
134
- // Fallback to original import if parsing fails
135
- return zodImport.importStatement;
136
- })
137
- .join('\n') + '\n\n';
138
- // Add type definitions for imported zod schemas if needed
139
- // This is a simplified approach - you might want to make this more sophisticated
121
+ // For projects without zod dependency, fallback to any types instead of importing zod
122
+ // This prevents TypeScript errors when zod is not installed
140
123
  const customTypeDefinitions = model.zodImports
141
124
  .flatMap(zodImport => zodImport.types)
142
125
  .filter((type, index, array) => array.indexOf(type) === index) // Remove duplicates
143
126
  .map(type => {
144
- // If it ends with 'Schema', create a corresponding type
127
+ // If it ends with 'Schema', create a corresponding type using any instead of zod
145
128
  if (type.endsWith('Schema')) {
146
129
  const typeName = type.replace('Schema', '');
147
- return `export type ${typeName} = z.infer<typeof ${type}>;`;
130
+ return `// Fallback type when zod is not available\nexport type ${typeName} = any;`;
148
131
  }
149
132
  return '';
150
133
  })