suparisma 1.0.0 → 1.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Suparisma
1
+ # Suparisma
2
2
  Supabase + Prisma!
3
3
 
4
4
  ![Suparisma Logo](https://vg-bunny-cdn.b-cdn.net/random/suparisma-banner.png)
@@ -155,6 +155,37 @@ export type SearchState = {
155
155
  clearQueries: () => void;
156
156
  };
157
157
 
158
+ /**
159
+ * Compare two values for sorting with proper type handling
160
+ */
161
+ function compareValues(a: any, b: any, direction: 'asc' | 'desc'): number {
162
+ // Handle undefined/null values
163
+ if (a === undefined || a === null) return direction === 'asc' ? -1 : 1;
164
+ if (b === undefined || b === null) return direction === 'asc' ? 1 : -1;
165
+
166
+ // Handle numbers properly to ensure numeric comparison
167
+ if (typeof a === 'number' && typeof b === 'number') {
168
+ return direction === 'asc'
169
+ ? a - b
170
+ : b - a;
171
+ }
172
+
173
+ // Handle dates (convert to timestamps for comparison)
174
+ if (a instanceof Date && b instanceof Date) {
175
+ return direction === 'asc'
176
+ ? a.getTime() - b.getTime()
177
+ : b.getTime() - a.getTime();
178
+ }
179
+
180
+ // Handle strings or mixed types with string conversion
181
+ const aStr = String(a);
182
+ const bStr = String(b);
183
+
184
+ return direction === 'asc'
185
+ ? aStr.localeCompare(bStr)
186
+ : bStr.localeCompare(aStr);
187
+ }
188
+
158
189
  /**
159
190
  * Convert a type-safe where filter to Supabase filter string
160
191
  */
@@ -316,11 +347,17 @@ export function applyOrderBy<T>(
316
347
 
317
348
  // Apply each order by clause
318
349
  let orderedQuery = query;
319
- for (const [key, direction] of Object.entries(orderBy)) {
320
- // @ts-ignore: Supabase typing issue
321
- orderedQuery = orderedQuery.order(key, {
322
- ascending: direction === 'asc'
323
- });
350
+
351
+ // Handle orderBy as array or single object
352
+ const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy];
353
+
354
+ for (const orderByClause of orderByArray) {
355
+ for (const [key, direction] of Object.entries(orderByClause)) {
356
+ // @ts-ignore: Supabase typing issue
357
+ orderedQuery = orderedQuery.order(key, {
358
+ ascending: direction === 'asc'
359
+ });
360
+ }
324
361
  }
325
362
 
326
363
  return orderedQuery;
@@ -387,9 +424,29 @@ export function createSuparismaHook<
387
424
  offset,
388
425
  } = options;
389
426
 
390
- // Compute the actual filter string from the type-safe where object or use legacy filter
391
- const computedFilter = where ? buildFilterString(where) : realtimeFilter;
392
-
427
+ // Refs to store the latest options for realtime handlers
428
+ const whereRef = useRef(where);
429
+ const orderByRef = useRef(orderBy);
430
+ const limitRef = useRef(limit);
431
+ const offsetRef = useRef(offset);
432
+
433
+ // Update refs whenever options change
434
+ useEffect(() => {
435
+ whereRef.current = where;
436
+ }, [where]);
437
+
438
+ useEffect(() => {
439
+ orderByRef.current = orderBy;
440
+ }, [orderBy]);
441
+
442
+ useEffect(() => {
443
+ limitRef.current = limit;
444
+ }, [limit]);
445
+
446
+ useEffect(() => {
447
+ offsetRef.current = offset;
448
+ }, [offset]);
449
+
393
450
  // Single data collection for holding results
394
451
  const [data, setData] = useState<TWithRelations[]>([]);
395
452
  const [error, setError] = useState<Error | null>(null);
@@ -773,14 +830,11 @@ export function createSuparismaHook<
773
830
 
774
831
  const channelId = channelName || \`changes_to_\${tableName}_\${Math.random().toString(36).substring(2, 15)}\`;
775
832
 
776
- // Store the current filter and options for closure
777
- const currentFilter = computedFilter;
778
- const currentWhere = where;
779
- const currentOrderBy = orderBy;
780
- const currentLimit = limit;
781
- const currentOffset = offset;
782
-
783
- console.log(\`Setting up subscription for \${tableName} with filter: \${currentFilter}\`);
833
+ // Use a dynamic filter string builder inside the event handler or rely on Supabase
834
+ // For the subscription filter, we must use the initial computedFilter or a stable one.
835
+ // However, for client-side logic (sorting, adding/removing from list), we use refs.
836
+ const initialComputedFilter = where ? buildFilterString(where) : realtimeFilter;
837
+ console.log(\`Setting up subscription for \${tableName} with initial filter: \${initialComputedFilter}\`);
784
838
 
785
839
  const channel = supabase
786
840
  .channel(channelId)
@@ -790,11 +844,17 @@ export function createSuparismaHook<
790
844
  event: '*',
791
845
  schema: 'public',
792
846
  table: tableName,
793
- filter: currentFilter,
847
+ filter: initialComputedFilter, // Subscription filter uses initial state
794
848
  },
795
849
  (payload) => {
796
850
  console.log(\`Received \${payload.eventType} event for \${tableName}\`, payload);
797
851
 
852
+ // Access current options via refs inside the event handler
853
+ const currentWhere = whereRef.current;
854
+ const currentOrderBy = orderByRef.current;
855
+ const currentLimit = limitRef.current;
856
+ const currentOffset = offsetRef.current; // Not directly used in handlers but good for consistency
857
+
798
858
  // Skip realtime updates when search is active
799
859
  if (isSearchingRef.current) return;
800
860
 
@@ -806,7 +866,7 @@ export function createSuparismaHook<
806
866
  console.log(\`Processing INSERT for \${tableName}\`, { newRecord });
807
867
 
808
868
  // Check if this record matches our filter if we have one
809
- if (currentWhere) {
869
+ if (currentWhere) { // Use ref value
810
870
  let matchesFilter = true;
811
871
 
812
872
  // Check each filter condition
@@ -838,27 +898,43 @@ export function createSuparismaHook<
838
898
  }
839
899
 
840
900
  // Add the new record to the data
841
- let newData = [newRecord, ...prev];
901
+ let newData = [...prev, newRecord]; // Changed: Use spread on prev for immutability
842
902
 
843
903
  // Apply ordering if specified
844
- if (currentOrderBy) {
845
- const [orderField, direction] = Object.entries(currentOrderBy)[0] || [];
846
- if (orderField) {
847
- newData = [...newData].sort((a, b) => {
848
- const aValue = a[orderField as keyof typeof a] ?? '';
849
- const bValue = b[orderField as keyof typeof b] ?? '';
850
-
851
- if (direction === 'asc') {
852
- return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
853
- } else {
854
- return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
904
+ if (currentOrderBy) { // Use ref value
905
+ // Convert orderBy to array format for consistency if it's an object
906
+ const orderByArray = Array.isArray(currentOrderBy)
907
+ ? currentOrderBy
908
+ : [currentOrderBy];
909
+
910
+ // Apply each sort in sequence
911
+ newData = [...newData].sort((a, b) => {
912
+ // Check each orderBy clause in sequence
913
+ for (const orderByClause of orderByArray) {
914
+ for (const [field, direction] of Object.entries(orderByClause)) {
915
+ const aValue = a[field as keyof typeof a];
916
+ const bValue = b[field as keyof typeof b];
917
+
918
+ // Skip if values are equal and move to next criterion
919
+ if (aValue === bValue) continue;
920
+
921
+ // Use the compareValues function for proper type handling
922
+ return compareValues(aValue, bValue, direction as 'asc' | 'desc');
855
923
  }
856
- });
857
- }
924
+ }
925
+ return 0; // Equal if all criteria match
926
+ });
927
+ } else if (hasCreatedAt) {
928
+ // Default sort by createdAt desc if no explicit sort but has timestamp
929
+ newData = [...newData].sort((a, b) => {
930
+ const aValue = a[createdAtField as keyof typeof a];
931
+ const bValue = b[createdAtField as keyof typeof b];
932
+ return compareValues(aValue, bValue, 'desc');
933
+ });
858
934
  }
859
935
 
860
936
  // Apply limit if specified
861
- if (currentLimit && currentLimit > 0) {
937
+ if (currentLimit && currentLimit > 0) { // Use ref value
862
938
  newData = newData.slice(0, currentLimit);
863
939
  }
864
940
 
@@ -874,6 +950,10 @@ export function createSuparismaHook<
874
950
  } else if (payload.eventType === 'UPDATE') {
875
951
  // Process update event
876
952
  setData((prev) => {
953
+ // Access current options via refs
954
+ const currentOrderBy = orderByRef.current;
955
+ const currentLimit = limitRef.current; // If needed for re-fetch logic on update
956
+
877
957
  // Skip if search is active
878
958
  if (isSearchingRef.current) {
879
959
  return prev;
@@ -886,15 +966,57 @@ export function createSuparismaHook<
886
966
  : item
887
967
  );
888
968
 
969
+ // Apply ordering again after update to ensure consistency
970
+ let sortedData = [...newData];
971
+
972
+ // Apply ordering if specified
973
+ if (currentOrderBy) { // Use ref value
974
+ // Convert orderBy to array format for consistency if it's an object
975
+ const orderByArray = Array.isArray(currentOrderBy)
976
+ ? currentOrderBy
977
+ : [currentOrderBy];
978
+
979
+ // Apply each sort in sequence
980
+ sortedData = sortedData.sort((a, b) => {
981
+ // Check each orderBy clause in sequence
982
+ for (const orderByClause of orderByArray) {
983
+ for (const [field, direction] of Object.entries(orderByClause)) {
984
+ const aValue = a[field as keyof typeof a];
985
+ const bValue = b[field as keyof typeof b];
986
+
987
+ // Skip if values are equal and move to next criterion
988
+ if (aValue === bValue) continue;
989
+
990
+ // Use the compareValues function for proper type handling
991
+ return compareValues(aValue, bValue, direction as 'asc' | 'desc');
992
+ }
993
+ }
994
+ return 0; // Equal if all criteria match
995
+ });
996
+ } else if (hasCreatedAt) {
997
+ // Default sort by createdAt desc if no explicit sort but has timestamp
998
+ sortedData = sortedData.sort((a, b) => {
999
+ const aValue = a[createdAtField as keyof typeof a];
1000
+ const bValue = b[createdAtField as keyof typeof b];
1001
+ return compareValues(aValue, bValue, 'desc');
1002
+ });
1003
+ }
1004
+
889
1005
  // Fetch the updated count after the data changes
890
1006
  // For updates, the count might not change but we fetch anyway to be consistent
891
1007
  setTimeout(() => fetchTotalCount(), 0);
892
1008
 
893
- return newData;
1009
+ return sortedData;
894
1010
  });
895
1011
  } else if (payload.eventType === 'DELETE') {
896
1012
  // Process delete event
897
1013
  setData((prev) => {
1014
+ // Access current options via refs
1015
+ const currentWhere = whereRef.current;
1016
+ const currentOrderBy = orderByRef.current;
1017
+ const currentLimit = limitRef.current;
1018
+ const currentOffset = offsetRef.current;
1019
+
898
1020
  // Skip if search is active
899
1021
  if (isSearchingRef.current) {
900
1022
  return prev;
@@ -913,21 +1035,61 @@ export function createSuparismaHook<
913
1035
  setTimeout(() => fetchTotalCount(), 0);
914
1036
 
915
1037
  // If we need to maintain the size with a limit
916
- if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) {
1038
+ if (currentLimit && currentLimit > 0 && filteredData.length < currentSize && currentSize === currentLimit) { // Use ref value
917
1039
  console.log(\`Record deleted with limit \${currentLimit}, will fetch additional record to maintain size\`);
918
1040
 
919
1041
  // Use setTimeout to ensure this state update completes first
920
1042
  setTimeout(() => {
921
1043
  findMany({
922
- where: currentWhere,
923
- orderBy: currentOrderBy,
924
- take: currentLimit,
925
- skip: currentOffset
1044
+ where: currentWhere, // Use ref value
1045
+ orderBy: currentOrderBy, // Use ref value
1046
+ take: currentLimit, // Use ref value
1047
+ skip: currentOffset // Use ref value (passed as skip to findMany)
926
1048
  });
927
1049
  }, 0);
1050
+
1051
+ // Return the filtered data without resizing for now
1052
+ // The findMany call above will update the data later
1053
+ return filteredData;
1054
+ }
1055
+
1056
+ // Re-apply ordering to maintain consistency
1057
+ let sortedData = [...filteredData];
1058
+
1059
+ // Apply ordering if specified
1060
+ if (currentOrderBy) { // Use ref value
1061
+ // Convert orderBy to array format for consistency if it's an object
1062
+ const orderByArray = Array.isArray(currentOrderBy)
1063
+ ? currentOrderBy
1064
+ : [currentOrderBy];
1065
+
1066
+ // Apply each sort in sequence
1067
+ sortedData = sortedData.sort((a, b) => {
1068
+ // Check each orderBy clause in sequence
1069
+ for (const orderByClause of orderByArray) {
1070
+ for (const [field, direction] of Object.entries(orderByClause)) {
1071
+ const aValue = a[field as keyof typeof a];
1072
+ const bValue = b[field as keyof typeof b];
1073
+
1074
+ // Skip if values are equal and move to next criterion
1075
+ if (aValue === bValue) continue;
1076
+
1077
+ // Use the compareValues function for proper type handling
1078
+ return compareValues(aValue, bValue, direction as 'asc' | 'desc');
1079
+ }
1080
+ }
1081
+ return 0; // Equal if all criteria match
1082
+ });
1083
+ } else if (hasCreatedAt) {
1084
+ // Default sort by createdAt desc if no explicit sort but has timestamp
1085
+ sortedData = sortedData.sort((a, b) => {
1086
+ const aValue = a[createdAtField as keyof typeof a];
1087
+ const bValue = b[createdAtField as keyof typeof b];
1088
+ return compareValues(aValue, bValue, 'desc');
1089
+ });
928
1090
  }
929
1091
 
930
- return filteredData;
1092
+ return sortedData;
931
1093
  });
932
1094
  }
933
1095
  }
@@ -942,7 +1104,7 @@ export function createSuparismaHook<
942
1104
  return () => {
943
1105
  console.log(\`Unsubscribing from \${channelId}\`);
944
1106
  if (channelRef.current) {
945
- channelRef.current.unsubscribe();
1107
+ supabase.removeChannel(channelRef.current); // Correct way to remove channel
946
1108
  channelRef.current = null;
947
1109
  }
948
1110
 
@@ -951,7 +1113,7 @@ export function createSuparismaHook<
951
1113
  searchTimeoutRef.current = null;
952
1114
  }
953
1115
  };
954
- }, [realtime, channelName, computedFilter]);
1116
+ }, [realtime, channelName, tableName, initialLoadRef]); // Removed where, orderBy, limit, offset from deps
955
1117
 
956
1118
  // Create a memoized options object to prevent unnecessary re-renders
957
1119
  const optionsRef = useRef({ where, orderBy, limit, offset });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma.",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -1,80 +0,0 @@
1
- // THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
2
- // Edit the generator script instead: scripts/generate-realtime-hooks.ts
3
-
4
- // Corrected import for core hook factory
5
- import { createSuparismaHook } from '../utils/core';
6
- import type {
7
- AuditLogWithRelations,
8
- AuditLogCreateInput,
9
- AuditLogUpdateInput,
10
- AuditLogWhereInput,
11
- AuditLogWhereUniqueInput,
12
- AuditLogOrderByInput,
13
- AuditLogHookApi,
14
- UseAuditLogOptions
15
- } from '../types/AuditLogTypes';
16
-
17
- /**
18
- * A Prisma-like hook for interacting with AuditLog records with real-time capabilities.
19
- *
20
- * This hook provides CRUD operations, real-time updates, and search functionality.
21
- *
22
- * @param options - Optional configuration options for the hook
23
- * @returns An object with data state and methods for interacting with AuditLog records
24
- *
25
- * @example
26
- * // Basic usage - get all AuditLog records with realtime updates
27
- * const auditlog = useSuparismaAuditLog();
28
- * const { data, loading, error } = auditlog;
29
- *
30
- * @example
31
- * // With filtering and ordering
32
- * const auditlog = useSuparismaAuditLog({
33
- * where: { active: true },
34
- * orderBy: { createdAt: 'desc' }, // Note: Using actual Prisma field name
35
- * limit: 10
36
- * });
37
- *
38
- * @example
39
- * // Create a new record
40
- * const result = await auditlog.create({
41
- * name: "Example Name",
42
- * // other fields...
43
- * });
44
- *
45
- * @example
46
- * // Update a record
47
- * const result = await auditlog.update({
48
- * where: { id: "123" },
49
- * data: { name: "Updated Name" }
50
- * });
51
- *
52
- * @example
53
- * // Delete a record
54
- * const result = await auditlog.delete({ id: "123" });
55
- *
56
- * @example
57
- * // Find records with specific criteria
58
- * const result = await auditlog.findMany({
59
- * where: { // filters },
60
- * orderBy: { // ordering },
61
- * take: 20 // limit
62
- * });
63
- */
64
- export const useSuparismaAuditLog = createSuparismaHook<
65
- AuditLogWithRelations,
66
- AuditLogWithRelations,
67
- AuditLogCreateInput,
68
- AuditLogUpdateInput,
69
- AuditLogWhereInput,
70
- AuditLogWhereUniqueInput,
71
- AuditLogOrderByInput
72
- >({
73
- tableName: 'AuditLog',
74
- hasCreatedAt: true,
75
- hasUpdatedAt: false,
76
- // Default values from schema
77
- defaultValues: {"id":"uuid(","createdAt":"now("},
78
- // Field name for createdAt from Prisma schema
79
- createdAtField: "createdAt"
80
- }) as unknown as (options?: UseAuditLogOptions) => AuditLogHookApi;
@@ -1,82 +0,0 @@
1
- // THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
2
- // Edit the generator script instead: scripts/generate-realtime-hooks.ts
3
-
4
- // Corrected import for core hook factory
5
- import { createSuparismaHook } from '../utils/core';
6
- import type {
7
- ThingWithRelations,
8
- ThingCreateInput,
9
- ThingUpdateInput,
10
- ThingWhereInput,
11
- ThingWhereUniqueInput,
12
- ThingOrderByInput,
13
- ThingHookApi,
14
- UseThingOptions
15
- } from '../types/ThingTypes';
16
-
17
- /**
18
- * A Prisma-like hook for interacting with Thing records with real-time capabilities.
19
- *
20
- * This hook provides CRUD operations, real-time updates, and search functionality.
21
- *
22
- * @param options - Optional configuration options for the hook
23
- * @returns An object with data state and methods for interacting with Thing records
24
- *
25
- * @example
26
- * // Basic usage - get all Thing records with realtime updates
27
- * const thing = useSuparismaThing();
28
- * const { data, loading, error } = thing;
29
- *
30
- * @example
31
- * // With filtering and ordering
32
- * const thing = useSuparismaThing({
33
- * where: { active: true },
34
- * orderBy: { createdAt: 'desc' }, // Note: Using actual Prisma field name
35
- * limit: 10
36
- * });
37
- *
38
- * @example
39
- * // Create a new record
40
- * const result = await thing.create({
41
- * name: "Example Name",
42
- * // other fields...
43
- * });
44
- *
45
- * @example
46
- * // Update a record
47
- * const result = await thing.update({
48
- * where: { id: "123" },
49
- * data: { name: "Updated Name" }
50
- * });
51
- *
52
- * @example
53
- * // Delete a record
54
- * const result = await thing.delete({ id: "123" });
55
- *
56
- * @example
57
- * // Find records with specific criteria
58
- * const result = await thing.findMany({
59
- * where: { // filters },
60
- * orderBy: { // ordering },
61
- * take: 20 // limit
62
- * });
63
- */
64
- export const useSuparismaThing = createSuparismaHook<
65
- ThingWithRelations,
66
- ThingWithRelations,
67
- ThingCreateInput,
68
- ThingUpdateInput,
69
- ThingWhereInput,
70
- ThingWhereUniqueInput,
71
- ThingOrderByInput
72
- >({
73
- tableName: 'Thing',
74
- hasCreatedAt: true,
75
- hasUpdatedAt: true,
76
- // Default values from schema
77
- defaultValues: {"id":"uuid(","someEnum":"ONE","createdAt":"now("},
78
- // Field name for createdAt from Prisma schema
79
- createdAtField: "createdAt",
80
- // Field name for updatedAt from Prisma schema
81
- updatedAtField: "updatedAt"
82
- }) as unknown as (options?: UseThingOptions) => ThingHookApi;
@@ -1,55 +0,0 @@
1
- // THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
2
- // Edit the generator script instead: scripts/generate-realtime-hooks.ts
3
-
4
- import { useSuparismaThing } from './hooks/useSuparismaThing';
5
- import { useSuparismaAuditLog } from './hooks/useSuparismaAuditLog';
6
- import type { UseThingOptions, ThingHookApi } from './types/ThingTypes';
7
- import type { UseAuditLogOptions, AuditLogHookApi } from './types/AuditLogTypes';
8
- export type { SuparismaOptions, SearchQuery, SearchState, FilterOperators } from './utils/core';
9
- export type { ThingWithRelations, ThingCreateInput, ThingUpdateInput, ThingWhereInput, ThingWhereUniqueInput, ThingOrderByInput, ThingHookApi, UseThingOptions } from './types/ThingTypes';
10
- export type { AuditLogWithRelations, AuditLogCreateInput, AuditLogUpdateInput, AuditLogWhereInput, AuditLogWhereUniqueInput, AuditLogOrderByInput, AuditLogHookApi, UseAuditLogOptions } from './types/AuditLogTypes';
11
-
12
- /**
13
- * Interface for all Suparisma hooks with dot notation access.
14
- * This provides IntelliSense for all available models.
15
- *
16
- * @example
17
- * // Access hooks for different models
18
- * const users = useSuparisma.user();
19
- * const posts = useSuparisma.post();
20
- */
21
- export interface SuparismaHooks {
22
- thing: (options?: UseThingOptions) => ThingHookApi;
23
- auditLog: (options?: UseAuditLogOptions) => AuditLogHookApi;
24
- }
25
-
26
- /**
27
- * Main Suparisma hook object with dot notation access to all model hooks.
28
- *
29
- * @example
30
- * // Get hooks for different models
31
- * import useSuparisma from './your-output-dir'; // e.g., from './suparisma/generated'
32
- *
33
- * // Access user model with all hook methods
34
- * const users = useSuparisma.user();
35
- * const { data, loading, error } = users;
36
- *
37
- * // Create a new record
38
- * await users.create({ name: "John" });
39
- *
40
- * // Delete a record
41
- * await users.delete({ id: "123" });
42
- *
43
- * @example
44
- * // Use with filtering and options
45
- * const admins = useSuparisma.user({
46
- * where: { role: 'admin' },
47
- * orderBy: { created_at: 'desc' }
48
- * });
49
- */
50
- const useSuparisma: SuparismaHooks = {
51
- thing: useSuparismaThing,
52
- auditLog: useSuparismaAuditLog,
53
- };
54
-
55
- export default useSuparisma;