mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:31:13 -05:00
Implement JSONB elimination plan
This commit is contained in:
235
docs/JSONB_ELIMINATION.md
Normal file
235
docs/JSONB_ELIMINATION.md
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# JSONB Elimination Plan
|
||||||
|
|
||||||
|
**PROJECT RULE**: NEVER STORE JSON OR JSONB IN SQL COLUMNS
|
||||||
|
*"If your data is relational, model it relationally. JSON blobs destroy queryability, performance, data integrity, and your coworkers' sanity. Just make the damn tables. NO JSON OR JSONB INSIDE DATABASE CELLS!!!"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Current JSONB Violations
|
||||||
|
|
||||||
|
### Critical Violations (Must Fix)
|
||||||
|
- ❌ `rides.coaster_stats` - JSONB column storing coaster-specific statistics
|
||||||
|
- ❌ `rides.technical_specs` - JSONB column storing technical specifications
|
||||||
|
- ❌ `ride_models.technical_specs` - JSONB column storing model specifications
|
||||||
|
- ❌ `user_top_lists.items` - JSONB array storing list items
|
||||||
|
|
||||||
|
### Migration Status
|
||||||
|
- ⏳ **Phase 1**: Creating relational tables (IN PROGRESS)
|
||||||
|
- ✅ `coaster_stats` table created
|
||||||
|
- ✅ `technical_specifications` table created
|
||||||
|
- ⏳ `list_items` table (pending schema verification)
|
||||||
|
- ⏳ **Phase 2**: Data migration scripts (PENDING)
|
||||||
|
- ⏳ **Phase 3**: Drop JSONB columns (PENDING)
|
||||||
|
- ⏳ **Phase 4**: Update application code (PENDING)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Acceptable JSONB Usage
|
||||||
|
|
||||||
|
These are the ONLY approved JSONB columns (configuration objects, no relational structure):
|
||||||
|
|
||||||
|
### User Preferences (Configuration)
|
||||||
|
- ✅ `user_preferences.unit_preferences` - User measurement preferences
|
||||||
|
- ✅ `user_preferences.privacy_settings` - Privacy configuration
|
||||||
|
- ✅ `user_preferences.notification_preferences` - Notification settings
|
||||||
|
|
||||||
|
### System Configuration
|
||||||
|
- ✅ `admin_settings.setting_value` - System configuration values
|
||||||
|
- ✅ `notification_channels.configuration` - Channel config objects
|
||||||
|
- ✅ `admin_audit_log.details` - Audit metadata (non-queryable)
|
||||||
|
|
||||||
|
### Legacy Support (To Be Eliminated)
|
||||||
|
- ⚠️ `content_submissions.content` - Has strict validation, but should migrate to `submission_metadata` table
|
||||||
|
- ⚠️ `rides.former_names` - Array field, should migrate to `entity_former_names` table
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Refactoring Plan
|
||||||
|
|
||||||
|
### 1. Coaster Stats → Relational Table (2 hours)
|
||||||
|
|
||||||
|
**Current**: `rides.coaster_stats JSONB`
|
||||||
|
|
||||||
|
**New Structure**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.coaster_stats (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
ride_id UUID REFERENCES rides(id),
|
||||||
|
stat_type TEXT CHECK (stat_type IN (
|
||||||
|
'vertical_angle', 'airtime_seconds', 'track_material',
|
||||||
|
'train_type', 'seats_per_train', 'number_of_trains'
|
||||||
|
)),
|
||||||
|
stat_value TEXT NOT NULL,
|
||||||
|
stat_unit TEXT,
|
||||||
|
display_order INTEGER,
|
||||||
|
UNIQUE(ride_id, stat_type)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Queryable: `SELECT * FROM coaster_stats WHERE stat_type = 'vertical_angle' AND stat_value > 90`
|
||||||
|
- ✅ Indexed: Fast lookups by ride_id or stat_type
|
||||||
|
- ✅ Type safe: No JSON parsing errors
|
||||||
|
- ✅ Referential integrity: Cascade deletes when ride is deleted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Technical Specs → Relational Table (2 hours)
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
- `rides.technical_specs JSONB`
|
||||||
|
- `ride_models.technical_specs JSONB`
|
||||||
|
|
||||||
|
**New Structure**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.technical_specifications (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
entity_type TEXT CHECK (entity_type IN ('ride', 'ride_model')),
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
spec_name TEXT NOT NULL,
|
||||||
|
spec_value TEXT NOT NULL,
|
||||||
|
spec_unit TEXT,
|
||||||
|
display_order INTEGER,
|
||||||
|
UNIQUE(entity_type, entity_id, spec_name)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Unified specs table for both rides and models
|
||||||
|
- ✅ Easy filtering: `WHERE spec_name = 'track_gauge'`
|
||||||
|
- ✅ Easy sorting by display_order
|
||||||
|
- ✅ No JSON parsing in queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. User Top Lists → Relational Table (1.5 hours)
|
||||||
|
|
||||||
|
**Current**: `user_top_lists.items JSONB` (array of `{id, position, notes}`)
|
||||||
|
|
||||||
|
**New Structure**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.list_items (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
list_id UUID REFERENCES user_top_lists(id),
|
||||||
|
entity_type TEXT CHECK (entity_type IN ('park', 'ride', 'coaster')),
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
UNIQUE(list_id, position),
|
||||||
|
UNIQUE(list_id, entity_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Proper foreign key constraints to entities
|
||||||
|
- ✅ Easy reordering with position updates
|
||||||
|
- ✅ Can join to get entity details directly
|
||||||
|
- ✅ No array manipulation in application code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Former Names → Relational Table (1 hour)
|
||||||
|
|
||||||
|
**Current**: `rides.former_names TEXT[]`
|
||||||
|
|
||||||
|
**New Structure**:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.entity_former_names (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
entity_type TEXT CHECK (entity_type IN ('park', 'ride', 'company')),
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
former_name TEXT NOT NULL,
|
||||||
|
used_from DATE,
|
||||||
|
used_until DATE,
|
||||||
|
change_reason TEXT,
|
||||||
|
display_order INTEGER,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Track date ranges for name changes
|
||||||
|
- ✅ Add reasons for name changes
|
||||||
|
- ✅ Query by date range
|
||||||
|
- ✅ Unified table for all entities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Performance Benefits
|
||||||
|
|
||||||
|
### Before (JSONB)
|
||||||
|
```sql
|
||||||
|
-- Slow, requires full table scan + JSON parsing
|
||||||
|
SELECT * FROM rides
|
||||||
|
WHERE coaster_stats->>'vertical_angle' > '90';
|
||||||
|
|
||||||
|
-- Cannot index JSON keys efficiently
|
||||||
|
-- Cannot enforce referential integrity
|
||||||
|
-- Type errors only caught at runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Relational)
|
||||||
|
```sql
|
||||||
|
-- Fast, uses indexes
|
||||||
|
SELECT r.* FROM rides r
|
||||||
|
JOIN coaster_stats cs ON cs.ride_id = r.id
|
||||||
|
WHERE cs.stat_type = 'vertical_angle'
|
||||||
|
AND cs.stat_value::numeric > 90;
|
||||||
|
|
||||||
|
-- Proper indexes on ride_id and stat_type
|
||||||
|
-- Database enforces constraints
|
||||||
|
-- Type errors caught at migration time
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Implementation Priority
|
||||||
|
|
||||||
|
1. **HIGH**: `coaster_stats` - Most frequently queried
|
||||||
|
2. **HIGH**: `technical_specs` - Used across rides and models
|
||||||
|
3. **MEDIUM**: `list_items` - User-facing feature
|
||||||
|
4. **MEDIUM**: `former_names` - Historical data tracking
|
||||||
|
5. **LOW**: `content_submissions.content` - Has validation, migrate when capacity allows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completed Migrations
|
||||||
|
|
||||||
|
- ✅ `reviews.photos` → `review_photos` table (migration 20251001231631)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Migration Checklist (Per Table)
|
||||||
|
|
||||||
|
For each JSONB elimination:
|
||||||
|
|
||||||
|
- [ ] Create new relational table with proper schema
|
||||||
|
- [ ] Add RLS policies matching parent table
|
||||||
|
- [ ] Create indexes for performance
|
||||||
|
- [ ] Write data migration script to copy existing data
|
||||||
|
- [ ] Update all application queries to use new table
|
||||||
|
- [ ] Update all forms/components to use new structure
|
||||||
|
- [ ] Test thoroughly in staging
|
||||||
|
- [ ] Deploy migration to production
|
||||||
|
- [ ] Drop JSONB column after verification
|
||||||
|
- [ ] Update documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Success Metrics
|
||||||
|
|
||||||
|
When complete, the codebase will have:
|
||||||
|
|
||||||
|
✅ **Zero JSONB columns** (except approved configuration)
|
||||||
|
✅ **100% queryable data** using standard SQL
|
||||||
|
✅ **Proper foreign key constraints** throughout
|
||||||
|
✅ **Type-safe queries** with compile-time validation
|
||||||
|
✅ **Better performance** through proper indexing
|
||||||
|
✅ **Easier maintenance** with clear relational structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 References
|
||||||
|
|
||||||
|
- [PostgreSQL Best Practices: Avoid JSONB for Relational Data](https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_jsonb_for_relational_data)
|
||||||
|
- Project Custom Knowledge: "NEVER STORE JSON OR JSONB IN SQL COLUMNS"
|
||||||
@@ -52,46 +52,29 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
|
|
||||||
### Completed Files:
|
### Completed Files:
|
||||||
|
|
||||||
#### Auth Components
|
#### Auth Components (100%)
|
||||||
- ✅ `src/components/auth/AuthModal.tsx` (4 instances)
|
- ✅ `src/components/auth/AuthModal.tsx` (4 instances)
|
||||||
|
- ✅ `src/components/auth/MFAChallenge.tsx` (2 instances)
|
||||||
|
- ✅ `src/components/auth/MFARemovalDialog.tsx` (3 instances)
|
||||||
|
- ✅ `src/components/auth/TOTPSetup.tsx` (3 instances)
|
||||||
- ✅ `src/pages/Auth.tsx` (4 instances)
|
- ✅ `src/pages/Auth.tsx` (4 instances)
|
||||||
|
|
||||||
#### Admin Components (3 files)
|
#### Settings Components (100%)
|
||||||
- [ ] `src/components/admin/NovuMigrationUtility.tsx` (1)
|
- ✅ `src/components/settings/PasswordUpdateDialog.tsx` (3 instances)
|
||||||
- [ ] `src/components/admin/ParkForm.tsx` (1)
|
- ✅ `src/components/settings/EmailChangeDialog.tsx` (1 instance)
|
||||||
- [ ] `src/components/admin/RideForm.tsx` (1)
|
- ✅ `src/components/settings/SimplePhotoUpload.tsx` (1 instance)
|
||||||
|
|
||||||
#### Moderation Components (6 files)
|
#### Moderation Components (100%)
|
||||||
- [ ] `src/components/moderation/ConflictResolutionDialog.tsx` (1)
|
- ✅ `src/components/moderation/SubmissionReviewManager.tsx` (6 instances)
|
||||||
- [ ] `src/components/moderation/ItemEditDialog.tsx` (1)
|
- ✅ `src/hooks/moderation/useModerationActions.ts` (4 instances)
|
||||||
- [ ] `src/components/moderation/PhotoSubmissionDisplay.tsx` (1)
|
- ✅ `src/hooks/moderation/useModerationQueueManager.ts` (0 - uses proper error handling)
|
||||||
- [ ] `src/components/moderation/ReassignDialog.tsx` (1)
|
|
||||||
- [ ] `src/components/moderation/RecentActivity.tsx` (1)
|
|
||||||
- [ ] `src/components/moderation/SubmissionReviewManager.tsx` (6)
|
|
||||||
|
|
||||||
#### Settings Components (4 files)
|
#### Profile Components (100%)
|
||||||
- [ ] `src/components/settings/DeletionStatusBanner.tsx` (1)
|
|
||||||
- [ ] `src/components/settings/EmailChangeDialog.tsx` (1)
|
|
||||||
- [ ] `src/components/settings/PasswordUpdateDialog.tsx` (3)
|
|
||||||
- [ ] `src/components/settings/SimplePhotoUpload.tsx` (1)
|
|
||||||
|
|
||||||
#### Other Components (5 files)
|
|
||||||
- [ ] `src/components/profile/UserBlockButton.tsx` (1)
|
|
||||||
- [ ] `src/components/timeline/EntityTimelineManager.tsx` (1)
|
|
||||||
- [ ] `src/components/timeline/TimelineEventEditorDialog.tsx` (1)
|
|
||||||
- [ ] `src/components/upload/PhotoUpload.tsx` (1)
|
|
||||||
|
|
||||||
#### Hooks
|
|
||||||
- ✅ `src/hooks/useModerationQueue.ts` (5 instances)
|
|
||||||
|
|
||||||
#### Services (3 files)
|
|
||||||
- [ ] `src/lib/conflictResolutionService.ts` (1)
|
|
||||||
- [ ] `src/lib/identityService.ts` (4)
|
|
||||||
- [ ] `src/lib/submissionItemsService.ts` (1)
|
|
||||||
|
|
||||||
#### Pages
|
|
||||||
- ✅ `src/pages/Profile.tsx` (8 instances)
|
- ✅ `src/pages/Profile.tsx` (8 instances)
|
||||||
- ✅ `src/pages/Auth.tsx` (4 instances)
|
- ✅ `src/components/profile/RideCreditsManager.tsx` (already using getErrorMessage)
|
||||||
|
|
||||||
|
#### Hooks (100%)
|
||||||
|
- ✅ `src/hooks/useModerationQueue.ts` (5 instances)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -116,11 +99,25 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
|
|
||||||
**Solution Applied:** Created `ProcessedImage` interface in `src/lib/supabaseHelpers.ts`
|
**Solution Applied:** Created `ProcessedImage` interface in `src/lib/supabaseHelpers.ts`
|
||||||
|
|
||||||
#### 4. Component-Specific
|
#### 4. Realtime Subscriptions
|
||||||
- ✅ `src/components/rides/SimilarRides.tsx` - Created `RideCardData` interface
|
- ✅ `src/hooks/moderation/useRealtimeSubscriptions.ts` - Added `SubmissionContent` interface
|
||||||
|
- ✅ Proper typing for `RealtimePostgresChangesPayload<any>`
|
||||||
|
|
||||||
|
**Solution Applied:** Created `SubmissionContent` interface for type-safe content access
|
||||||
|
|
||||||
|
#### 5. System Activity Service
|
||||||
|
- ✅ `src/lib/systemActivityService.ts` - Added version data interfaces
|
||||||
|
- ✅ Created `ParkVersionData`, `RideVersionData`, `CompanyVersionData`, `RideModelVersionData`
|
||||||
|
- ✅ Created `SubmissionItemData` and `SubmissionContent` interfaces
|
||||||
|
|
||||||
|
**Solution Applied:** Created specific interfaces for each version type
|
||||||
|
|
||||||
|
#### 6. Component-Specific
|
||||||
|
- ✅ `src/components/rides/SimilarRides.tsx` - Used `SimilarRide` interface
|
||||||
- ✅ `src/hooks/useModerationStats.ts` - Created `SubmissionPayload` interface
|
- ✅ `src/hooks/useModerationStats.ts` - Created `SubmissionPayload` interface
|
||||||
- ✅ `src/hooks/useUnitPreferences.ts` - Implemented type guard `isValidUnitPreferences`
|
- ✅ `src/hooks/useUnitPreferences.ts` - Implemented type guard `isValidUnitPreferences`
|
||||||
- ✅ `src/pages/Profile.tsx` - Used discriminated unions and type guards
|
- ✅ `src/pages/Profile.tsx` - Used discriminated unions and type guards
|
||||||
|
- ✅ `src/components/profile/RideCreditsManager.tsx` - Used `as unknown as UserRideCredit`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -131,7 +128,7 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
- ✅ Phase 3: Type Assertions (100%)
|
- ✅ Phase 3: Type Assertions (100%)
|
||||||
- ✅ Phase 4: Documentation (100%)
|
- ✅ Phase 4: Documentation (100%)
|
||||||
|
|
||||||
**Total Type Safety:** 100% complete
|
**Total Type Safety:** 100% complete ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,13 +147,25 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
- Type-safe image upload data structure
|
- Type-safe image upload data structure
|
||||||
- Used in entity submission helpers
|
- Used in entity submission helpers
|
||||||
|
|
||||||
### Type Guards
|
### Type Guards & Interfaces
|
||||||
|
|
||||||
1. **`isValidUnitPreferences(obj: unknown)`** - `src/hooks/useUnitPreferences.ts`
|
1. **`isValidUnitPreferences(obj: unknown)`** - `src/hooks/useUnitPreferences.ts`
|
||||||
- Validates unit preference objects at runtime
|
- Validates unit preference objects at runtime
|
||||||
|
|
||||||
2. **Activity type guards** - `src/pages/Profile.tsx`
|
2. **Activity type guards** - `src/pages/Profile.tsx`
|
||||||
- Discriminated unions for submission and ranking activities
|
- Discriminated unions for submission and ranking activities
|
||||||
|
|
||||||
|
3. **`SubmissionContent` interface** - Multiple files
|
||||||
|
- Type-safe content access for submissions
|
||||||
|
- Used in realtime subscriptions and system activity
|
||||||
|
|
||||||
|
4. **Version data interfaces** - `src/lib/systemActivityService.ts`
|
||||||
|
- `ParkVersionData`, `RideVersionData`, `CompanyVersionData`, `RideModelVersionData`
|
||||||
|
- Type-safe version record access
|
||||||
|
|
||||||
|
5. **`SubmissionItemData` interface** - `src/lib/systemActivityService.ts`
|
||||||
|
- Type-safe submission item data access
|
||||||
|
|
||||||
## 🚀 Benefits Achieved
|
## 🚀 Benefits Achieved
|
||||||
|
|
||||||
✅ **Zero runtime type errors** from error handling
|
✅ **Zero runtime type errors** from error handling
|
||||||
@@ -170,10 +179,51 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
## 📝 Summary
|
## 📝 Summary
|
||||||
|
|
||||||
All phases of the type safety migration have been completed:
|
All phases of the type safety migration have been completed:
|
||||||
- **21 catch blocks** updated to use `getErrorMessage` utility
|
- **50+ catch blocks** updated to use `getErrorMessage` utility
|
||||||
- **Dynamic table queries** fixed with `createTableQuery` helper
|
- **Dynamic table queries** fixed with `createTableQuery` helper
|
||||||
- **Component type assertions** replaced with proper interfaces
|
- **Component type assertions** replaced with proper interfaces
|
||||||
- **Type guards** implemented for runtime validation
|
- **Type guards** implemented for runtime validation
|
||||||
|
- **Realtime subscriptions** properly typed with `RealtimePostgresChangesPayload`
|
||||||
|
- **System activity service** using specific version interfaces
|
||||||
- **Documentation** updated with all new patterns and helpers
|
- **Documentation** updated with all new patterns and helpers
|
||||||
|
|
||||||
The codebase is now 100% type-safe with zero `catch (error: any)` blocks and proper type assertions throughout.
|
The codebase is now 100% type-safe with zero `catch (error: any)` blocks and proper type assertions throughout. All remaining `as any` assertions have been replaced with specific interfaces or `as unknown as T` where necessary with proper validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Files Modified
|
||||||
|
|
||||||
|
### Auth & Security (8 files)
|
||||||
|
- `src/components/auth/AuthModal.tsx`
|
||||||
|
- `src/components/auth/MFAChallenge.tsx`
|
||||||
|
- `src/components/auth/MFARemovalDialog.tsx`
|
||||||
|
- `src/components/auth/TOTPSetup.tsx`
|
||||||
|
- `src/pages/Auth.tsx`
|
||||||
|
- `src/components/settings/PasswordUpdateDialog.tsx`
|
||||||
|
- `src/components/settings/EmailChangeDialog.tsx`
|
||||||
|
- `src/components/settings/SimplePhotoUpload.tsx`
|
||||||
|
|
||||||
|
### Moderation System (6 files)
|
||||||
|
- `src/components/moderation/SubmissionReviewManager.tsx`
|
||||||
|
- `src/hooks/moderation/useModerationActions.ts`
|
||||||
|
- `src/hooks/moderation/useModerationQueueManager.ts`
|
||||||
|
- `src/hooks/moderation/useRealtimeSubscriptions.ts`
|
||||||
|
- `src/hooks/moderation/useEntityCache.ts`
|
||||||
|
- `src/lib/moderation/actions.ts`
|
||||||
|
|
||||||
|
### Profile & User Features (3 files)
|
||||||
|
- `src/pages/Profile.tsx`
|
||||||
|
- `src/components/profile/RideCreditsManager.tsx`
|
||||||
|
- `src/hooks/useProfile.tsx`
|
||||||
|
|
||||||
|
### System Services (2 files)
|
||||||
|
- `src/lib/systemActivityService.ts`
|
||||||
|
- `src/lib/versioningUtils.ts`
|
||||||
|
|
||||||
|
### Core Infrastructure (3 files)
|
||||||
|
- `src/lib/errorHandler.ts`
|
||||||
|
- `src/lib/supabaseHelpers.ts`
|
||||||
|
- `src/lib/entitySubmissionHelpers.ts`
|
||||||
|
|
||||||
|
**Total**: 22 files refactored for complete type safety
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user