suparisma 1.0.3 → 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 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 = Partial<${modelName}WithRelations>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suparisma",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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": {