Context
This issue tracks the refactoring work to migrate from a string-based preview column in the project table to using a polymorphic file table pattern.
Related PR: #766
Requested by: @fingertips18
Current State
- ✅ File table created with polymorphic pattern (parent_table, parent_id, role)
- ❌ Project table still has
preview string column
- ❌ Backend and frontend code still expect preview as a string field
- Migration to drop preview column exists but should be deferred until this refactoring is complete
Goal
Implement a flexible polymorphic file management system where:
- Projects can have multiple files with different roles (preview, attachments, etc.)
- File metadata is centralized in the file table
- Easy to extend to other entities (users, etc.)
Implementation Steps
1. Backend Changes (Go)
1.1 Create File Domain Model
// apps/backend/internal/domain/file.go
type File struct {
ID uuid.UUID
ParentTable string
ParentID uuid.UUID
Role string
Key string
URL string
Type string
Size int64
CreatedAt time.Time
UpdatedAt time.Time
}
1.2 Create File Repository
FindByParent(parentTable, parentID, role) - Get files for a specific parent and role
Create(file) - Create new file record
Delete(id) - Delete file record
FindByID(id) - Get file by ID
1.3 Update Project Model
Add computed field or association:
type Project struct {
// ... existing fields
PreviewFile *File `json:"preview_file,omitempty"`
}
1.4 Update Project Repository
Modify GetByID and List to JOIN with file table:
SELECT p.*, f.* FROM project p
LEFT JOIN file f ON f.parent_table = 'project'
AND f.parent_id = p.id
AND f.role = 'preview'
1.5 Update API Handlers
- Modify GET /projects responses to include preview_file
- Add file upload endpoints if not already present
- Handle file creation when project is created/updated
2. Frontend Changes (React/TypeScript)
2.1 Update TypeScript Types
// apps/admin/src/types/file.ts
export interface File {
id: string;
parentTable: string;
parentId: string;
role: string;
key: string;
url: string;
type: string;
size: number;
createdAt: string;
updatedAt: string;
}
// apps/admin/src/types/project.ts
export interface Project {
// ... existing fields
preview?: string; // Keep for backward compatibility initially
previewFile?: File; // New field
}
2.2 Update Project Forms
apps/admin/src/pages/project/add/_components/form.tsx
apps/admin/src/pages/project/edit/_components/form.tsx
- Update file upload handlers to create file records with
parent_table='project' and role='preview'
2.3 Update Display Components
apps/admin/src/pages/project/view/index.tsx
apps/frontend/src/pages/project/card.tsx
- Use
previewFile?.url instead of preview where applicable
2.4 Add Fallback Logic
Handle transition period where some projects have string preview, others have previewFile:
const previewUrl = project.previewFile?.url ?? project.preview;
3. Data Migration
3.1 Create Migration Script
-- Migrate existing preview URLs to file table
INSERT INTO file (id, parent_table, parent_id, role, key, url, type, size, created_at, updated_at)
SELECT
gen_random_uuid(),
'project',
id,
'preview',
SUBSTRING(preview FROM '[^/]+$'), -- Extract filename from URL
preview,
'image/jpeg', -- Default type, adjust as needed
0, -- Size unknown, can be updated later
NOW(),
NOW()
FROM project
WHERE preview IS NOT NULL AND preview != '';
3.2 Drop Preview Column
Once migration is complete and tested:
-- apps/backend/internal/database/migrations/YYYYMMDDHHMMSS_drop_project_preview.up.sql
ALTER TABLE project DROP COLUMN preview;
4. Testing Checklist
5. Deployment Steps
- Merge backend changes (API backward compatible)
- Run data migration script
- Deploy frontend changes
- Monitor for issues
- After stable, run migration to drop preview column
- Clean up fallback code in frontend
Notes
- Keep the migration that drops the preview column but don't apply it until this refactoring is complete
- Consider adding file deletion cleanup (delete from storage when file record is deleted)
- Future: Extend to support multiple files per project (galleries, attachments)
- Future: Add file validation (size limits, type restrictions)
Definition of Done
Context
This issue tracks the refactoring work to migrate from a string-based
previewcolumn in the project table to using a polymorphic file table pattern.Related PR: #766
Requested by: @fingertips18
Current State
previewstring columnGoal
Implement a flexible polymorphic file management system where:
Implementation Steps
1. Backend Changes (Go)
1.1 Create File Domain Model
1.2 Create File Repository
FindByParent(parentTable, parentID, role)- Get files for a specific parent and roleCreate(file)- Create new file recordDelete(id)- Delete file recordFindByID(id)- Get file by ID1.3 Update Project Model
Add computed field or association:
1.4 Update Project Repository
Modify
GetByIDandListto JOIN with file table:1.5 Update API Handlers
2. Frontend Changes (React/TypeScript)
2.1 Update TypeScript Types
2.2 Update Project Forms
apps/admin/src/pages/project/add/_components/form.tsxapps/admin/src/pages/project/edit/_components/form.tsxparent_table='project'androle='preview'2.3 Update Display Components
apps/admin/src/pages/project/view/index.tsxapps/frontend/src/pages/project/card.tsxpreviewFile?.urlinstead ofpreviewwhere applicable2.4 Add Fallback Logic
Handle transition period where some projects have string preview, others have previewFile:
3. Data Migration
3.1 Create Migration Script
3.2 Drop Preview Column
Once migration is complete and tested:
4. Testing Checklist
5. Deployment Steps
Notes
Definition of Done