suparisma 1.0.2 → 1.0.4
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 +198 -0
- package/dist/generators/coreGenerator.js +82 -0
- package/dist/generators/typeGenerator.js +58 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -303,6 +303,204 @@ const { data } = useSuparisma.thing({
|
|
|
303
303
|
});
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
+
### Array Filtering
|
|
307
|
+
|
|
308
|
+
Suparisma provides powerful operators for filtering array fields (e.g., `String[]`, `Int[]`, etc.):
|
|
309
|
+
|
|
310
|
+
```prisma
|
|
311
|
+
model Post {
|
|
312
|
+
id String @id @default(uuid())
|
|
313
|
+
title String
|
|
314
|
+
tags String[] // Array field
|
|
315
|
+
ratings Int[] // Array field
|
|
316
|
+
// ... other fields
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Array Operators
|
|
321
|
+
|
|
322
|
+
| Operator | Description | Example Usage |
|
|
323
|
+
|----------|-------------|---------------|
|
|
324
|
+
| `has` | Array contains **ANY** of the specified items | `tags: { has: ["react", "typescript"] }` |
|
|
325
|
+
| `hasSome` | Array contains **ANY** of the specified items (alias for `has`) | `tags: { hasSome: ["react", "vue"] }` |
|
|
326
|
+
| `hasEvery` | Array contains **ALL** of the specified items | `tags: { hasEvery: ["react", "typescript"] }` |
|
|
327
|
+
| `isEmpty` | Array is empty or not empty | `tags: { isEmpty: false }` |
|
|
328
|
+
|
|
329
|
+
#### Array Filtering Examples
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
// Find posts that have ANY of these tags
|
|
333
|
+
const { data: reactOrVuePosts } = useSuparisma.post({
|
|
334
|
+
where: {
|
|
335
|
+
tags: { has: ["react", "vue", "angular"] }
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// Returns posts with tags like: ["react"], ["vue"], ["react", "typescript"], etc.
|
|
339
|
+
|
|
340
|
+
// Find posts that have ALL of these tags
|
|
341
|
+
const { data: fullStackPosts } = useSuparisma.post({
|
|
342
|
+
where: {
|
|
343
|
+
tags: { hasEvery: ["react", "typescript", "nodejs"] }
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
// Returns posts that contain all three tags (and possibly more)
|
|
347
|
+
|
|
348
|
+
// Find posts with any of these ratings
|
|
349
|
+
const { data: highRatedPosts } = useSuparisma.post({
|
|
350
|
+
where: {
|
|
351
|
+
ratings: { hasSome: [4, 5] }
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
// Returns posts with arrays containing 4 or 5: [3, 4], [5], [1, 2, 4, 5], etc.
|
|
355
|
+
|
|
356
|
+
// Find posts with no tags
|
|
357
|
+
const { data: untaggedPosts } = useSuparisma.post({
|
|
358
|
+
where: {
|
|
359
|
+
tags: { isEmpty: true }
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Find posts that have tags (non-empty)
|
|
364
|
+
const { data: taggedPosts } = useSuparisma.post({
|
|
365
|
+
where: {
|
|
366
|
+
tags: { isEmpty: false }
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Combine array filtering with other conditions
|
|
371
|
+
const { data: featuredReactPosts } = useSuparisma.post({
|
|
372
|
+
where: {
|
|
373
|
+
tags: { has: ["react"] },
|
|
374
|
+
featured: true,
|
|
375
|
+
ratings: { hasEvery: [4, 5] } // Must have both 4 AND 5 ratings
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Exact array match (regular equality)
|
|
380
|
+
const { data: exactMatch } = useSuparisma.post({
|
|
381
|
+
where: {
|
|
382
|
+
tags: ["react", "typescript"] // Exact match: only this array
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### Real-World Array Filtering Scenarios
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// E-commerce: Find products by multiple categories
|
|
391
|
+
const { data: products } = useSuparisma.product({
|
|
392
|
+
where: {
|
|
393
|
+
categories: { has: ["electronics", "gaming"] } // Any of these categories
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Social Media: Find posts with specific hashtags
|
|
398
|
+
const { data: trendingPosts } = useSuparisma.post({
|
|
399
|
+
where: {
|
|
400
|
+
hashtags: { hasSome: ["trending", "viral", "popular"] }
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Project Management: Find tasks assigned to specific team members
|
|
405
|
+
const { data: myTasks } = useSuparisma.task({
|
|
406
|
+
where: {
|
|
407
|
+
assignedTo: { has: ["john@company.com"] } // Tasks assigned to John
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Content Management: Find articles with all required tags
|
|
412
|
+
const { data: completeArticles } = useSuparisma.article({
|
|
413
|
+
where: {
|
|
414
|
+
requiredTags: { hasEvery: ["reviewed", "approved", "published"] }
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Interactive Array Filtering Component
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
import { useState } from "react";
|
|
423
|
+
import useSuparisma from '../generated';
|
|
424
|
+
|
|
425
|
+
function ProductFilter() {
|
|
426
|
+
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
|
427
|
+
const [filterMode, setFilterMode] = useState<'any' | 'all'>('any');
|
|
428
|
+
|
|
429
|
+
const { data: products, loading } = useSuparisma.product({
|
|
430
|
+
where: selectedCategories.length > 0 ? {
|
|
431
|
+
categories: filterMode === 'any'
|
|
432
|
+
? { has: selectedCategories } // ANY of selected categories
|
|
433
|
+
: { hasEvery: selectedCategories } // ALL of selected categories
|
|
434
|
+
} : undefined
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const handleCategoryToggle = (category: string) => {
|
|
438
|
+
setSelectedCategories(prev =>
|
|
439
|
+
prev.includes(category)
|
|
440
|
+
? prev.filter(c => c !== category)
|
|
441
|
+
: [...prev, category]
|
|
442
|
+
);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div>
|
|
447
|
+
{/* Filter Mode Toggle */}
|
|
448
|
+
<div className="mb-4">
|
|
449
|
+
<label className="mr-4">
|
|
450
|
+
<input
|
|
451
|
+
type="radio"
|
|
452
|
+
value="any"
|
|
453
|
+
checked={filterMode === 'any'}
|
|
454
|
+
onChange={(e) => setFilterMode(e.target.value as 'any')}
|
|
455
|
+
/>
|
|
456
|
+
Match ANY categories
|
|
457
|
+
</label>
|
|
458
|
+
<label>
|
|
459
|
+
<input
|
|
460
|
+
type="radio"
|
|
461
|
+
value="all"
|
|
462
|
+
checked={filterMode === 'all'}
|
|
463
|
+
onChange={(e) => setFilterMode(e.target.value as 'all')}
|
|
464
|
+
/>
|
|
465
|
+
Match ALL categories
|
|
466
|
+
</label>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
{/* Category Checkboxes */}
|
|
470
|
+
<div className="mb-4">
|
|
471
|
+
{['electronics', 'clothing', 'books', 'home', 'sports'].map(category => (
|
|
472
|
+
<label key={category} className="mr-4">
|
|
473
|
+
<input
|
|
474
|
+
type="checkbox"
|
|
475
|
+
checked={selectedCategories.includes(category)}
|
|
476
|
+
onChange={() => handleCategoryToggle(category)}
|
|
477
|
+
/>
|
|
478
|
+
{category}
|
|
479
|
+
</label>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{/* Results */}
|
|
484
|
+
<div>
|
|
485
|
+
{loading ? (
|
|
486
|
+
<p>Loading products...</p>
|
|
487
|
+
) : (
|
|
488
|
+
<div>
|
|
489
|
+
<h3>Found {products?.length || 0} products</h3>
|
|
490
|
+
{products?.map(product => (
|
|
491
|
+
<div key={product.id}>
|
|
492
|
+
<h4>{product.name}</h4>
|
|
493
|
+
<p>Categories: {product.categories.join(', ')}</p>
|
|
494
|
+
</div>
|
|
495
|
+
))}
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
306
504
|
### Sorting Data
|
|
307
505
|
|
|
308
506
|
Sort data using Prisma-like ordering:
|
|
@@ -43,6 +43,18 @@ export type SupabaseQueryBuilder = ReturnType<ReturnType<typeof supabase.from>['
|
|
|
43
43
|
* @example
|
|
44
44
|
* // Posts with titles containing "news"
|
|
45
45
|
* { title: { contains: "news" } }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Array contains ANY of these items (overlaps)
|
|
49
|
+
* { tags: { has: ["typescript", "react"] } }
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Array contains ALL of these items (contains)
|
|
53
|
+
* { categories: { hasEvery: ["tech", "programming"] } }
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Array contains ANY of these items (same as 'has')
|
|
57
|
+
* { tags: { hasSome: ["javascript", "python"] } }
|
|
46
58
|
*/
|
|
47
59
|
export type FilterOperators<T> = {
|
|
48
60
|
/** Equal to value */
|
|
@@ -67,6 +79,16 @@ export type FilterOperators<T> = {
|
|
|
67
79
|
startsWith?: string;
|
|
68
80
|
/** String ends with value (case insensitive) */
|
|
69
81
|
endsWith?: string;
|
|
82
|
+
|
|
83
|
+
// Array-specific operators
|
|
84
|
+
/** Array contains ANY of the specified items (for array fields) */
|
|
85
|
+
has?: T extends Array<infer U> ? U[] : never;
|
|
86
|
+
/** Array contains ANY of the specified items (alias for 'has') */
|
|
87
|
+
hasSome?: T extends Array<infer U> ? U[] : never;
|
|
88
|
+
/** Array contains ALL of the specified items (for array fields) */
|
|
89
|
+
hasEvery?: T extends Array<infer U> ? U[] : never;
|
|
90
|
+
/** Array is empty (for array fields) */
|
|
91
|
+
isEmpty?: T extends Array<any> ? boolean : never;
|
|
70
92
|
};
|
|
71
93
|
|
|
72
94
|
// Type for a single field in an advanced where filter
|
|
@@ -239,6 +261,35 @@ export function buildFilterString<T>(where?: T): string | undefined {
|
|
|
239
261
|
if ('endsWith' in advancedOps && advancedOps.endsWith !== undefined) {
|
|
240
262
|
filters.push(\`\${key}=ilike.%\${advancedOps.endsWith}\`);
|
|
241
263
|
}
|
|
264
|
+
|
|
265
|
+
// Array-specific operators
|
|
266
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
267
|
+
// Array contains ANY of the specified items (overlaps)
|
|
268
|
+
const arrayValue = JSON.stringify(advancedOps.has);
|
|
269
|
+
filters.push(\`\${key}=ov.\${arrayValue}\`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
273
|
+
// Array contains ALL of the specified items (contains)
|
|
274
|
+
const arrayValue = JSON.stringify(advancedOps.hasEvery);
|
|
275
|
+
filters.push(\`\${key}=cs.\${arrayValue}\`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
279
|
+
// Array contains ANY of the specified items (overlaps)
|
|
280
|
+
const arrayValue = JSON.stringify(advancedOps.hasSome);
|
|
281
|
+
filters.push(\`\${key}=ov.\${arrayValue}\`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
285
|
+
if (advancedOps.isEmpty) {
|
|
286
|
+
// Check if array is empty
|
|
287
|
+
filters.push(\`\${key}=eq.{}\`);
|
|
288
|
+
} else {
|
|
289
|
+
// Check if array is not empty
|
|
290
|
+
filters.push(\`\${key}=neq.{}\`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
242
293
|
} else {
|
|
243
294
|
// Simple equality
|
|
244
295
|
filters.push(\`\${key}=eq.\${value}\`);
|
|
@@ -316,6 +367,37 @@ export function applyFilter<T>(
|
|
|
316
367
|
// @ts-ignore: Supabase typing issue
|
|
317
368
|
filteredQuery = filteredQuery.ilike(key, \`%\${advancedOps.endsWith}\`);
|
|
318
369
|
}
|
|
370
|
+
|
|
371
|
+
// Array-specific operators
|
|
372
|
+
if ('has' in advancedOps && advancedOps.has !== undefined) {
|
|
373
|
+
// Array contains ANY of the specified items (overlaps)
|
|
374
|
+
// @ts-ignore: Supabase typing issue
|
|
375
|
+
filteredQuery = filteredQuery.overlaps(key, advancedOps.has);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if ('hasEvery' in advancedOps && advancedOps.hasEvery !== undefined) {
|
|
379
|
+
// Array contains ALL of the specified items (contains)
|
|
380
|
+
// @ts-ignore: Supabase typing issue
|
|
381
|
+
filteredQuery = filteredQuery.contains(key, advancedOps.hasEvery);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if ('hasSome' in advancedOps && advancedOps.hasSome !== undefined) {
|
|
385
|
+
// Array contains ANY of the specified items (overlaps)
|
|
386
|
+
// @ts-ignore: Supabase typing issue
|
|
387
|
+
filteredQuery = filteredQuery.overlaps(key, advancedOps.hasSome);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if ('isEmpty' in advancedOps && advancedOps.isEmpty !== undefined) {
|
|
391
|
+
if (advancedOps.isEmpty) {
|
|
392
|
+
// Check if array is empty
|
|
393
|
+
// @ts-ignore: Supabase typing issue
|
|
394
|
+
filteredQuery = filteredQuery.eq(key, []);
|
|
395
|
+
} else {
|
|
396
|
+
// Check if array is not empty
|
|
397
|
+
// @ts-ignore: Supabase typing issue
|
|
398
|
+
filteredQuery = filteredQuery.neq(key, []);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
319
401
|
} else {
|
|
320
402
|
// Simple equality
|
|
321
403
|
// @ts-ignore: Supabase typing issue
|
|
@@ -119,7 +119,7 @@ function generateModelTypesFile(model) {
|
|
|
119
119
|
// Edit the generator script instead
|
|
120
120
|
|
|
121
121
|
import type { ${modelName} } from '@prisma/client';
|
|
122
|
-
import type { ModelResult, SuparismaOptions, SearchQuery, SearchState } from '../utils/core';
|
|
122
|
+
import type { ModelResult, SuparismaOptions, SearchQuery, SearchState, FilterOperators } from '../utils/core';
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* Extended ${modelName} type that includes relation fields.
|
|
@@ -208,8 +208,64 @@ ${withRelationsProps
|
|
|
208
208
|
.join(',\n')}
|
|
209
209
|
* }
|
|
210
210
|
* });
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* // Array filtering (for array fields)
|
|
214
|
+
* ${modelName.toLowerCase()}.findMany({
|
|
215
|
+
* where: {
|
|
216
|
+
* // Array contains specific items
|
|
217
|
+
${withRelationsProps
|
|
218
|
+
.filter((p) => p.includes('[]'))
|
|
219
|
+
.slice(0, 1)
|
|
220
|
+
.map((p) => {
|
|
221
|
+
const field = p.trim().split(':')[0].trim();
|
|
222
|
+
return ` * ${field}: { has: ["item1", "item2"] }`;
|
|
223
|
+
})
|
|
224
|
+
.join(',\n')}
|
|
225
|
+
* }
|
|
226
|
+
* });
|
|
211
227
|
*/
|
|
212
|
-
export type ${modelName}WhereInput =
|
|
228
|
+
export type ${modelName}WhereInput = {
|
|
229
|
+
${model.fields
|
|
230
|
+
.filter((field) => !relationObjectFields.includes(field.name) && !foreignKeyFields.includes(field.name))
|
|
231
|
+
.map((field) => {
|
|
232
|
+
const isOptional = true; // All where fields are optional
|
|
233
|
+
let baseType;
|
|
234
|
+
switch (field.type) {
|
|
235
|
+
case 'Int':
|
|
236
|
+
case 'Float':
|
|
237
|
+
baseType = 'number';
|
|
238
|
+
break;
|
|
239
|
+
case 'Boolean':
|
|
240
|
+
baseType = 'boolean';
|
|
241
|
+
break;
|
|
242
|
+
case 'DateTime':
|
|
243
|
+
baseType = 'string'; // ISO date string
|
|
244
|
+
break;
|
|
245
|
+
case 'Json':
|
|
246
|
+
baseType = 'any'; // Or a more specific structured type if available
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
// Covers String, Enum names (e.g., "SomeEnum"), Bytes, Decimal, etc.
|
|
250
|
+
baseType = 'string';
|
|
251
|
+
}
|
|
252
|
+
const finalType = field.isList ? `${baseType}[]` : baseType;
|
|
253
|
+
const filterType = `${finalType} | FilterOperators<${finalType}>`;
|
|
254
|
+
return ` ${field.name}${isOptional ? '?' : ''}: ${filterType};`;
|
|
255
|
+
})
|
|
256
|
+
.concat(
|
|
257
|
+
// Add foreign key fields
|
|
258
|
+
foreignKeyFields.map((field) => {
|
|
259
|
+
const fieldInfo = model.fields.find((f) => f.name === field);
|
|
260
|
+
if (fieldInfo) {
|
|
261
|
+
const baseType = fieldInfo.type === 'Int' ? 'number' : 'string';
|
|
262
|
+
const filterType = `${baseType} | FilterOperators<${baseType}>`;
|
|
263
|
+
return ` ${field}?: ${filterType};`;
|
|
264
|
+
}
|
|
265
|
+
return '';
|
|
266
|
+
}).filter(Boolean))
|
|
267
|
+
.join('\n')}
|
|
268
|
+
};
|
|
213
269
|
|
|
214
270
|
/**
|
|
215
271
|
* Unique identifier for finding a specific ${modelName} record.
|
package/package.json
CHANGED