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 +51 -2
- package/dist/generators/coreGenerator.js +137 -10
- package/dist/generators/hookGenerator.js +14 -0
- package/dist/generators/typeGenerator.js +39 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|