suparisma 1.2.2 → 1.2.3

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
@@ -26,6 +26,8 @@ A powerful, typesafe React hook generator for Supabase, driven by your Prisma sc
26
26
  - [Array Filtering](#array-filtering)
27
27
  - [Sorting Data](#sorting-data)
28
28
  - [Pagination](#pagination)
29
+ - [Field Selection (select)](#field-selection-select)
30
+ - [Including Relations (include)](#including-relations-include)
29
31
  - [Search Functionality](#search-functionality)
30
32
  - [Enabling Search](#enabling-search)
31
33
  - [Search Methods](#search-methods)
@@ -733,6 +735,53 @@ const { data: page2 } = useSuparisma.thing({
733
735
  const { data, count } = useSuparisma.thing();
734
736
  ```
735
737
 
738
+ ### Field Selection (select)
739
+
740
+ Use the `select` option to only return specific fields, reducing payload size:
741
+
742
+ ```tsx
743
+ // Only get id and name fields
744
+ const { data: things } = useSuparisma.thing({
745
+ select: { id: true, name: true }
746
+ });
747
+ // Returns: [{ id: "123", name: "Thing 1" }, ...]
748
+
749
+ // Combine with filtering
750
+ const { data: activeThings } = useSuparisma.thing({
751
+ where: { someEnum: "ONE" },
752
+ select: { id: true, name: true, someNumber: true }
753
+ });
754
+ ```
755
+
756
+ ### Including Relations (include)
757
+
758
+ Use the `include` option to fetch related records (foreign key relations):
759
+
760
+ ```tsx
761
+ // Include all fields from a related model
762
+ const { data: posts } = useSuparisma.post({
763
+ include: { author: true }
764
+ });
765
+ // Returns: [{ id: "123", title: "...", author: { id: "456", name: "John" } }, ...]
766
+
767
+ // Include specific fields from a relation
768
+ const { data: posts } = useSuparisma.post({
769
+ include: {
770
+ author: {
771
+ select: { id: true, name: true }
772
+ }
773
+ }
774
+ });
775
+
776
+ // Combine select and include
777
+ const { data: posts } = useSuparisma.post({
778
+ select: { id: true, title: true },
779
+ include: { author: true, comments: true }
780
+ });
781
+ ```
782
+
783
+ **Note:** The relation names in `include` should match your Prisma schema relation field names.
784
+
736
785
  ### Search Functionality
737
786
 
738
787
  Suparisma provides powerful PostgreSQL full-text search capabilities with automatic RPC function generation and type-safe search methods. Search is enabled per field using annotations in your Prisma schema.
@@ -1525,8 +1574,8 @@ const { data } = useSuparisma.thing({
1525
1574
  | `limit` | `number` | Maximum number of records to return |
1526
1575
  | `offset` | `number` | Number of records to skip for pagination |
1527
1576
  | `realtime` | `boolean` | Enable/disable real-time updates |
1528
- | `select` | `object` | Fields to include in the response |
1529
- | `include` | `object` | Related records to include |
1577
+ | `select` | `object` | Fields to include in the response. Use `{ fieldName: true }` syntax |
1578
+ | `include` | `object` | Related records to include. Use `{ relationName: true }` or `{ relationName: { select: {...} } }` |
1530
1579
  | `search` | `object` | Full-text search configuration |
1531
1580
 
1532
1581
  ### Hook Return Value
@@ -169,7 +169,36 @@ export type AdvancedWhereInput<T> = {
169
169
  * limit: 10
170
170
  * });
171
171
  */
172
- export type SuparismaOptions<TWhereInput, TOrderByInput> = {
172
+ /**
173
+ * Select input type - specify which fields to return
174
+ * Use true to include a field, or use an object for relations
175
+ * @example
176
+ * // Select specific fields
177
+ * { id: true, name: true, email: true }
178
+ *
179
+ * @example
180
+ * // Select fields with relations
181
+ * { id: true, name: true, posts: true }
182
+ */
183
+ export type SelectInput<T> = {
184
+ [K in keyof T]?: boolean;
185
+ };
186
+
187
+ /**
188
+ * Include input type - specify which relations to include
189
+ * @example
190
+ * // Include a relation with all fields
191
+ * { posts: true }
192
+ *
193
+ * @example
194
+ * // Include a relation with specific fields
195
+ * { posts: { select: { id: true, title: true } } }
196
+ */
197
+ export type IncludeInput = {
198
+ [key: string]: boolean | { select?: Record<string, boolean> };
199
+ };
200
+
201
+ export type SuparismaOptions<TWhereInput, TOrderByInput, TSelectInput = Record<string, boolean>> = {
173
202
  /** Whether to enable realtime updates (default: true) */
174
203
  realtime?: boolean;
175
204
  /** Custom channel name for realtime subscription */
@@ -184,6 +213,16 @@ export type SuparismaOptions<TWhereInput, TOrderByInput> = {
184
213
  limit?: number;
185
214
  /** Offset for pagination (skip records) */
186
215
  offset?: number;
216
+ /**
217
+ * Select specific fields to return. Reduces payload size.
218
+ * @example { id: true, name: true, email: true }
219
+ */
220
+ select?: TSelectInput;
221
+ /**
222
+ * Include related records (foreign key relations).
223
+ * @example { posts: true } or { posts: { select: { id: true, title: true } } }
224
+ */
225
+ include?: IncludeInput;
187
226
  };
188
227
 
189
228
  /**
@@ -733,6 +772,82 @@ function matchesFilter<T>(record: any, filter: T): boolean {
733
772
  return conditions.every(condition => condition);
734
773
  }
735
774
 
775
+ /**
776
+ * Build a Supabase select string from select and include options.
777
+ *
778
+ * @param select - Object specifying which fields to select { field: true }
779
+ * @param include - Object specifying which relations to include { relation: true }
780
+ * @returns A Supabase-compatible select string
781
+ *
782
+ * @example
783
+ * // Select specific fields
784
+ * buildSelectString({ id: true, name: true }) // Returns "id,name"
785
+ *
786
+ * @example
787
+ * // Include relations
788
+ * buildSelectString(undefined, { posts: true }) // Returns "*,posts(*)"
789
+ *
790
+ * @example
791
+ * // Select fields and include relations with specific fields
792
+ * buildSelectString({ id: true, name: true }, { posts: { select: { id: true, title: true } } })
793
+ * // Returns "id,name,posts(id,title)"
794
+ */
795
+ export function buildSelectString<TSelect, TInclude>(
796
+ select?: TSelect,
797
+ include?: TInclude
798
+ ): string {
799
+ const parts: string[] = [];
800
+
801
+ // Handle select - if provided, only return specified fields
802
+ if (select && typeof select === 'object') {
803
+ const selectedFields = Object.entries(select)
804
+ .filter(([_, value]) => value === true)
805
+ .map(([key]) => key);
806
+
807
+ if (selectedFields.length > 0) {
808
+ parts.push(...selectedFields);
809
+ }
810
+ }
811
+
812
+ // Handle include - add related records
813
+ if (include && typeof include === 'object') {
814
+ for (const [relationName, relationValue] of Object.entries(include)) {
815
+ if (relationValue === true) {
816
+ // Include all fields from the relation
817
+ parts.push(\`\${relationName}(*)\`);
818
+ } else if (typeof relationValue === 'object' && relationValue !== null) {
819
+ // Include specific fields from the relation
820
+ const relationOptions = relationValue as { select?: Record<string, boolean> };
821
+ if (relationOptions.select) {
822
+ const relationFields = Object.entries(relationOptions.select)
823
+ .filter(([_, value]) => value === true)
824
+ .map(([key]) => key);
825
+
826
+ if (relationFields.length > 0) {
827
+ parts.push(\`\${relationName}(\${relationFields.join(',')})\`);
828
+ } else {
829
+ parts.push(\`\${relationName}(*)\`);
830
+ }
831
+ } else {
832
+ parts.push(\`\${relationName}(*)\`);
833
+ }
834
+ }
835
+ }
836
+ }
837
+
838
+ // If no select specified but include is, we need to include base table fields too
839
+ if (parts.length === 0) {
840
+ return '*';
841
+ }
842
+
843
+ // If only include was specified (no select), we need all base fields plus relations
844
+ if (!select && include) {
845
+ return '*,' + parts.join(',');
846
+ }
847
+
848
+ return parts.join(',');
849
+ }
850
+
736
851
  /**
737
852
  * Apply order by to the query builder
738
853
  */
@@ -828,13 +943,19 @@ export function createSuparismaHook<
828
943
  orderBy,
829
944
  limit,
830
945
  offset,
946
+ select,
947
+ include,
831
948
  } = options;
832
949
 
950
+ // Build the select string once for reuse
951
+ const selectString = buildSelectString(select, include);
952
+
833
953
  // Refs to store the latest options for realtime handlers
834
954
  const whereRef = useRef(where);
835
955
  const orderByRef = useRef(orderBy);
836
956
  const limitRef = useRef(limit);
837
957
  const offsetRef = useRef(offset);
958
+ const selectStringRef = useRef(selectString);
838
959
 
839
960
  // Update refs whenever options change
840
961
  useEffect(() => {
@@ -853,6 +974,10 @@ export function createSuparismaHook<
853
974
  offsetRef.current = offset;
854
975
  }, [offset]);
855
976
 
977
+ useEffect(() => {
978
+ selectStringRef.current = selectString;
979
+ }, [selectString]);
980
+
856
981
  // Single data collection for holding results
857
982
  const [data, setData] = useState<TWithRelations[]>([]);
858
983
  const [error, setError] = useState<Error | null>(null);
@@ -1198,7 +1323,8 @@ export function createSuparismaHook<
1198
1323
  setLoading(true);
1199
1324
  setError(null);
1200
1325
 
1201
- let query = supabase.from(tableName).select('*');
1326
+ // Use selectString for field selection (includes relations if specified)
1327
+ let query = supabase.from(tableName).select(selectString);
1202
1328
 
1203
1329
  // Apply where conditions if provided
1204
1330
  if (params?.where) {
@@ -1285,7 +1411,7 @@ export function createSuparismaHook<
1285
1411
 
1286
1412
  const { data, error } = await supabase
1287
1413
  .from(tableName)
1288
- .select('*')
1414
+ .select(selectString)
1289
1415
  .eq(primaryKey, value)
1290
1416
  .maybeSingle();
1291
1417
 
@@ -1608,7 +1734,7 @@ export function createSuparismaHook<
1608
1734
  }, [realtime, channelName, tableName]); // NEVER include 'where' - subscription should persist
1609
1735
 
1610
1736
  // Create a memoized options object to prevent unnecessary re-renders
1611
- const optionsRef = useRef({ where, orderBy, limit, offset });
1737
+ const optionsRef = useRef({ where, orderBy, limit, offset, selectString });
1612
1738
 
1613
1739
  // Compare current options with previous options
1614
1740
  const optionsChanged = useCallback(() => {
@@ -1623,16 +1749,17 @@ export function createSuparismaHook<
1623
1749
  whereStr !== prevWhereStr ||
1624
1750
  orderByStr !== prevOrderByStr ||
1625
1751
  limit !== optionsRef.current.limit ||
1626
- offset !== optionsRef.current.offset;
1752
+ offset !== optionsRef.current.offset ||
1753
+ selectString !== optionsRef.current.selectString;
1627
1754
 
1628
1755
  if (hasChanged) {
1629
1756
  // Update the ref with the new values
1630
- optionsRef.current = { where, orderBy, limit, offset };
1757
+ optionsRef.current = { where, orderBy, limit, offset, selectString };
1631
1758
  return true;
1632
1759
  }
1633
1760
 
1634
1761
  return false;
1635
- }, [where, orderBy, limit, offset]);
1762
+ }, [where, orderBy, limit, offset, selectString]);
1636
1763
 
1637
1764
  // Load initial data and refetch when options change (BUT NEVER TOUCH SUBSCRIPTION)
1638
1765
  useEffect(() => {
@@ -1756,7 +1883,7 @@ export function createSuparismaHook<
1756
1883
  const { data: result, error } = await supabase
1757
1884
  .from(tableName)
1758
1885
  .insert([itemWithDefaults])
1759
- .select();
1886
+ .select(selectString);
1760
1887
 
1761
1888
  if (error) throw error;
1762
1889
 
@@ -1848,7 +1975,7 @@ export function createSuparismaHook<
1848
1975
  .from(tableName)
1849
1976
  .update(itemWithDefaults)
1850
1977
  .eq(primaryKey, value)
1851
- .select();
1978
+ .select(selectString);
1852
1979
 
1853
1980
  if (error) throw error;
1854
1981
 
@@ -1903,7 +2030,7 @@ export function createSuparismaHook<
1903
2030
  // First fetch the record to return it
1904
2031
  const { data: recordToDelete } = await supabase
1905
2032
  .from(tableName)
1906
- .select('*')
2033
+ .select(selectString)
1907
2034
  .eq(primaryKey, value)
1908
2035
  .maybeSingle();
1909
2036
 
@@ -42,6 +42,8 @@ import type {
42
42
  ${modelName}WhereInput,
43
43
  ${modelName}WhereUniqueInput,
44
44
  ${modelName}OrderByInput,
45
+ ${modelName}SelectInput,
46
+ ${modelName}IncludeInput,
45
47
  ${modelName}HookApi,
46
48
  Use${modelName}Options
47
49
  } from '../types/${modelName}Types';
@@ -92,6 +94,18 @@ import type {
92
94
  * orderBy: { // ordering },
93
95
  * take: 20 // limit
94
96
  * });
97
+ *
98
+ * @example
99
+ * // Select specific fields only
100
+ * const ${modelName.toLowerCase()} = ${config_1.HOOK_NAME_PREFIX}${modelName}({
101
+ * select: { id: true, name: true }
102
+ * });
103
+ *
104
+ * @example
105
+ * // Include related records
106
+ * const ${modelName.toLowerCase()} = ${config_1.HOOK_NAME_PREFIX}${modelName}({
107
+ * include: { relatedModel: true }
108
+ * });
95
109
  */
96
110
  export const ${config_1.HOOK_NAME_PREFIX}${modelName} = createSuparismaHook<
97
111
  ${modelName}WithRelations,
@@ -352,6 +352,40 @@ export type ${modelName}OrderByInput = {
352
352
  [key in keyof ${modelName}WithRelations]?: 'asc' | 'desc';
353
353
  };
354
354
 
355
+ /**
356
+ * Select specific fields to return from ${modelName} queries.
357
+ * Set fields to \`true\` to include them in the response.
358
+ *
359
+ * @example
360
+ * // Only return id and name
361
+ * ${modelName.toLowerCase()}.findMany({
362
+ * select: { id: true, name: true }
363
+ * });
364
+ */
365
+ export type ${modelName}SelectInput = {
366
+ [key in keyof ${modelName}WithRelations]?: boolean;
367
+ };
368
+
369
+ /**
370
+ * Include related records when querying ${modelName}.
371
+ * Set relation names to \`true\` to include all fields, or use an object to select specific fields.
372
+ *
373
+ * @example
374
+ * // Include all fields from a relation
375
+ * ${modelName.toLowerCase()}.findMany({
376
+ * include: { relatedModel: true }
377
+ * });
378
+ *
379
+ * @example
380
+ * // Include specific fields from a relation
381
+ * ${modelName.toLowerCase()}.findMany({
382
+ * include: { relatedModel: { select: { id: true, name: true } } }
383
+ * });
384
+ */
385
+ export type ${modelName}IncludeInput = {
386
+ [key: string]: boolean | { select?: Record<string, boolean> };
387
+ };
388
+
355
389
  /**
356
390
  * Result type for operations that return a single ${modelName} record.
357
391
  */
@@ -364,8 +398,12 @@ export type ${modelName}ManyResult = ModelResult<${modelName}WithRelations[]>;
364
398
 
365
399
  /**
366
400
  * Configuration options for the ${modelName} hook.
401
+ * Includes where filters, ordering, pagination, and field selection.
367
402
  */
368
- export type Use${modelName}Options = SuparismaOptions<${modelName}WhereInput, ${modelName}OrderByInput>;
403
+ export type Use${modelName}Options = SuparismaOptions<${modelName}WhereInput, ${modelName}OrderByInput, ${modelName}SelectInput> & {
404
+ /** Include related records (foreign key relations) */
405
+ include?: ${modelName}IncludeInput;
406
+ };
369
407
 
370
408
  /**
371
409
  * The complete API for interacting with ${modelName} records.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Opinionated typesafe React realtime CRUD hooks generator for all your Supabase tables, powered by Prisma. Works with Next.js, Remix, React Native, and Expo.",
5
5
  "main": "dist/index.js",
6
6
  "repository": {