Skip to content

Asset Content Category Implementation Plan

Overview

Add a content_category field to assets to distinguish storage mechanisms: - binary: Files stored in S3 (current behavior) - text: Content stored directly in the database

This is extensible for future types (e.g., url, embed).


Files to Modify

Backend

File Changes
backend/internal/db/migrations/005_asset_content_category.sql New migration (create)
backend/internal/models/asset.go Add fields + new request type
backend/internal/db/asset_repository.go Update interface
backend/internal/db/asset.go Update SQL queries
backend/internal/services/asset.go Add CreateTextAsset, modify existing methods
backend/internal/handlers/asset.go Add text asset endpoints

API

File Changes
api/components/schemas/assets.yaml Add ContentCategory enum, update Asset schema
api/paths/assets.yaml Add text asset endpoints

Frontend

File Changes
frontend/src/pages/dashboard/assets/asset-upload.tsx Add category selector + text input UI
frontend/src/pages/dashboard/assets/asset-list.tsx Display content category

Implementation Steps

1. Database Migration (005_asset_content_category.sql)

-- Add content_category column (default 'binary' for existing assets)
ALTER TABLE assets
ADD COLUMN content_category VARCHAR(20) NOT NULL DEFAULT 'binary'
CHECK (content_category IN ('binary', 'text'));

-- Add text_content column for storing text directly
ALTER TABLE assets ADD COLUMN text_content TEXT;

-- Make storage_key nullable (not needed for text assets)
ALTER TABLE assets ALTER COLUMN storage_key DROP NOT NULL;

-- Add consistency constraint
ALTER TABLE assets ADD CONSTRAINT assets_content_consistency CHECK (
    (content_category = 'binary' AND storage_key IS NOT NULL AND storage_key != '') OR
    (content_category = 'text' AND text_content IS NOT NULL)
);

-- Index for filtering
CREATE INDEX IF NOT EXISTS idx_assets_content_category ON assets(content_category);

2. Backend Model (models/asset.go)

Add to Asset struct: - ContentCategory string - 'binary' or 'text' - StorageKey *string - make nullable (pointer) - TextContent *string - content for text assets

Add constants:

const (
    ContentCategoryBinary = "binary"
    ContentCategoryText   = "text"
    MaxTextContentBytes   = 1 * 1024 * 1024 // 1 MB
)

Add new request type:

type CreateTextAssetRequest struct {
    OrgID       string
    Name        string
    Description string
    ContentType string  // e.g., text/plain, text/markdown
    TextContent string
}

3. Repository Updates (db/asset.go)

Update all queries to include content_category and text_content columns. Use sql.NullString for nullable storage_key and text_content.

4. Service Layer (services/asset.go)

Add CreateTextAsset() method: - Validate org exists - Check license quotas (count text bytes as storage) - Enforce 1 MB max for text content - Create asset with content_category='text' - Store content in text_content column - No S3 interaction

Add UpdateTextContent() method: - Allow editing text content after creation - Recalculate size_bytes on update - Enforce 1 MB max limit

Modify DeleteAsset(): - Only call S3 delete for binary assets

Modify GetDownloadURL(): - Return error for text assets (use content endpoint instead)

5. Handler Updates (handlers/asset.go)

Add new endpoints: - POST /orgs/{org_id}/assets/text - Create text asset - GET /orgs/{org_id}/assets/{id}/content - Get text content - PUT /orgs/{org_id}/assets/{id}/content - Update text content

6. API Schema Updates

Add to assets.yaml:

ContentCategory:
  type: string
  enum: [binary, text]

CreateTextAssetRequest:
  type: object
  required: [name, content_type, text_content]
  properties:
    name: {type: string}
    description: {type: string}
    content_type: {type: string}
    text_content: {type: string}

Update Asset schema: - Add content_category (required) - Make storage_key nullable - Add text_content (nullable)

7. Frontend Updates

asset-upload.tsx: - Add radio/toggle: "Binary (File)" vs "Text" - When Text selected: - Hide file dropzone - Show content type dropdown (text/plain, text/markdown, text/html, application/json) - Show textarea for content - Call new createTextAsset API

asset-list.tsx: - Add "Category" column with badge (binary/text) - For text assets: show "View" instead of download


Verification

  1. Run migration and verify existing assets have content_category='binary'
  2. Create binary asset - should work as before with S3 upload
  3. Create text asset - should save to DB without S3 interaction
  4. Create text asset > 1 MB - should fail with size limit error
  5. List assets - both types should appear with category badge
  6. Download binary asset - returns presigned S3 URL
  7. Get text asset content - returns text from database
  8. Update text asset content - should update content and recalculate size
  9. Delete both types - binary deletes from S3, text only from DB
  10. Quota enforcement - text content bytes count toward storage limit

Future Extensibility

To add new categories (e.g., 'url', 'embed'): 1. Add to migration CHECK constraint 2. Add column for new data (e.g., url_reference) 3. Add new request type and service method 4. Add new API endpoint 5. Update frontend UI mode