# 02. Design shared data models for all platforms meta: id: native-business-logic-migration-02 feature: native-business-logic-migration priority: P0 depends_on: [native-business-logic-migration-01] tags: [design, data-models] objective: - Design platform-agnostic data models that can be implemented natively on each platform deliverables: - Data model specification document - Entity relationship diagram - Type mapping guide (TypeScript → Swift/Kotlin/C) - Database schema design steps: - Review TypeScript types from src/types/feed.ts - Identify core entities: Feed, FeedItem, FeedSubscription, SearchResult - Design normalized database schema - Create type mapping documentation - Define value types vs reference types - Design enumeration types for each platform - Document serialization/deserialization requirements acceptance_criteria: - All data models documented with properties and types - Entity relationships clearly defined - Type mapping guide complete for all three platforms - Database schema designed with proper indexing - Value types identified (Date, UUID, Enums) notes: - Use UUID for identifiers across all platforms - Date handling needs platform-specific implementation - Consider memory footprint for large feed items - Plan for future extensibility --- # Shared Data Models Design ## Overview This document defines platform-agnostic data models for RSSuper, enabling native implementations on iOS (Swift), Android (Kotlin), and Linux (C). The models are designed for SQLite persistence with full-text search support. --- ## 1. Core Entities ### 1.1 FeedItem Represents a single article or episode from a feed. | Property | Type | Required | Description | |----------|------|----------|-------------| | id | UUID | Yes | Unique identifier | | title | String | Yes | Item title | | link | String | No | URL to full article | | description | String | No | Short summary | | content | String | No | Full content (may be large) | | author | String | No | Author name | | published | DateTime | No | Publication date | | updated | DateTime | No | Last update date | | categories | String[] | No | Category tags | | enclosure | Enclosure | No | Media attachment | | guid | String | No | Global unique identifier from feed | | subscriptionId | UUID | Yes | Parent subscription | | subscriptionTitle | String | No | Denormalized for display | #### Enclosure (nested) | Property | Type | Required | Description | |----------|------|----------|-------------| | url | String | Yes | Media URL | | type | String | Yes | MIME type (audio/mpeg, image/jpeg, etc.) | | length | Int64 | No | File size in bytes | ### 1.2 FeedSubscription Represents a feed subscription with configuration. | Property | Type | Required | Description | |----------|------|----------|-------------| | id | UUID | Yes | Unique identifier | | url | String | Yes | Feed URL (unique) | | title | String | Yes | Display title | | category | String | No | User-defined category | | enabled | Boolean | Yes | Active/inactive | | fetchInterval | Int32 | Yes | Fetch interval in minutes | | createdAt | DateTime | Yes | Creation timestamp | | updatedAt | DateTime | Yes | Last modification | | lastFetchedAt | DateTime | No | Last successful fetch | | nextFetchAt | DateTime | No | Next scheduled fetch | | error | String | No | Last error message | | httpAuth | HttpAuth | No | HTTP authentication | #### HttpAuth (nested) | Property | Type | Required | Description | |----------|------|----------|-------------| | username | String | Yes | Username | | password | String | Yes | Password (should be encrypted) | ### 1.3 SearchHistoryItem | Property | Type | Required | Description | |----------|------|----------|-------------| | id | UUID | Yes | Unique identifier | | query | String | Yes | Search query text | | timestamp | DateTime | Yes | When query was executed | --- ## 2. Entity Relationship Diagram ``` ┌─────────────────────┐ ┌─────────────────────┐ │ FeedSubscription │ │ SearchHistory │ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │ │ id (PK) │ │ url (UNIQUE) │ │ query │ │ title │ │ timestamp │ │ category │ └─────────────────────┘ │ enabled │ │ fetch_interval │ ┌─────────────────────┐ │ created_at │ │ FeedItem │ │ updated_at │ ├─────────────────────┤ │ last_fetched_at │ │ id (PK) │ │ next_fetch_at │──────┐ │ subscription_id(FK)│◄──┐ │ error │ │ │ title │ │ │ http_auth_username │ │ │ link │ │ │ http_auth_password │ │ │ description │ │ └─────────────────────┘ │ │ content │ │ │ │ author │ │ │ │ published │ │ │ │ updated │ │ │ │ guid (UNIQUE) │ │ │ │ enclosure_url │ │ │ │ enclosure_type │ │ │ │ enclosure_length │ │ │ │ categories (JSON) │ │ │ │ created_at │ │ │ └─────────────────────┘ │ │ │ └────────────────────────────────┘ ``` --- ## 3. Database Schema Design ### 3.1 subscriptions ```sql CREATE TABLE subscriptions ( id TEXT PRIMARY KEY, url TEXT UNIQUE NOT NULL, title TEXT NOT NULL, category TEXT, enabled INTEGER NOT NULL DEFAULT 1, fetch_interval INTEGER NOT NULL DEFAULT 60, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_fetched_at INTEGER, next_fetch_at INTEGER, error TEXT, http_auth_username TEXT, http_auth_password TEXT ); CREATE INDEX idx_subscriptions_url ON subscriptions(url); CREATE INDEX idx_subscriptions_category ON subscriptions(category); CREATE INDEX idx_subscriptions_next_fetch ON subscriptions(next_fetch_at) WHERE enabled = 1; CREATE INDEX idx_subscriptions_enabled ON subscriptions(enabled); ``` ### 3.2 feed_items ```sql CREATE TABLE feed_items ( id TEXT PRIMARY KEY, subscription_id TEXT NOT NULL, title TEXT NOT NULL, link TEXT, description TEXT, content TEXT, author TEXT, published INTEGER, updated INTEGER, guid TEXT, enclosure_url TEXT, enclosure_type TEXT, enclosure_length INTEGER, categories TEXT, created_at INTEGER NOT NULL, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE ); CREATE UNIQUE INDEX idx_feed_items_guid ON feed_items(guid) WHERE guid IS NOT NULL; CREATE INDEX idx_feed_items_subscription ON feed_items(subscription_id); CREATE INDEX idx_feed_items_published ON feed_items(published DESC); CREATE INDEX idx_feed_items_created ON feed_items(created_at DESC); ``` ### 3.3 search_history ```sql CREATE TABLE search_history ( id TEXT PRIMARY KEY, query TEXT NOT NULL, timestamp INTEGER NOT NULL ); CREATE INDEX idx_search_history_timestamp ON search_history(timestamp DESC); ``` ### 3.4 Full-Text Search (FTS5) ```sql CREATE VIRTUAL TABLE feed_items_fts USING fts5( title, description, content, author, categories, content='feed_items', content_rowid='rowid' ); CREATE VIRTUAL TABLE subscriptions_fts USING fts5( title, url, category, content='subscriptions', content_rowid='rowid' ); ``` --- ## 4. Type Mapping Guide ### 4.1 Primitive Types | TypeScript | Swift (Kotlin/C) | SQLite | Notes | |------------|------------------|--------|-------| | `string` | `String` (`String`/`char*`) | TEXT | UTF-8 encoded | | `number` | `Int`/`Double` (`Int`/`Double`/`int64_t`) | INTEGER/REAL | Use REAL for decimals | | `boolean` | `Bool` (`Boolean`/`int`) | INTEGER | 0 or 1 | | `Date` | `Date` (`LocalDateTime`/`time_t`) | INTEGER | Unix timestamp (milliseconds) | | `UUID` | `UUID` (`UUID`/`uuid_t`) | TEXT | String format: "550e8400-e29b-41d4-a716-446655440000" | | `string[]` | `[String]` (`List`/`char**`) | TEXT | JSON array | ### 4.2 Core Model Mapping #### FeedItem | Property | TypeScript | Swift | Kotlin | C (struct) | |----------|------------|-------|--------|------------| | id | `string` | `String` | `String` | `char id[37]` | | title | `string` | `String` | `String` | `char title[512]` | | link | `string?` | `String?` | `String?` | `char* link` | | description | `string?` | `String?` | `String?` | `char* description` | | content | `string?` | `String?` | `String?` | `char* content` | | author | `string?` | `String?` | `String?` | `char* author` | | published | `Date?` | `Date?` | `Long?` | `int64_t published` | | updated | `Date?` | `Date?` | `Long?` | `int64_t updated` | | categories | `string[]?` | `[String]?` | `String?` | `char* categories` | | enclosure | `Enclosure?` | `Enclosure?` | `Enclosure?` | `Enclosure* enclosure` | | guid | `string?` | `String?` | `String?` | `char* guid` | | subscriptionId | `string` | `String` | `String` | `char subscription_id[37]` | #### FeedSubscription | Property | TypeScript | Swift | Kotlin | C (struct) | |----------|------------|-------|--------|------------| | id | `string` | `String` | `String` | `char id[37]` | | url | `string` | `String` | `String` | `char url[2048]` | | title | `string` | `String` | `String` | `char title[512]` | | category | `string?` | `String?` | `String?` | `char* category` | | enabled | `boolean` | `Bool` | `Boolean` | `int enabled` | | fetchInterval | `number` | `Int` | `Int` | `int fetch_interval` | | createdAt | `Date` | `Date` | `Long` | `int64_t created_at` | | updatedAt | `Date` | `Date` | `Long` | `int64_t updated_at` | | lastFetchedAt | `Date?` | `Date?` | `Long?` | `int64_t last_fetched_at` | | nextFetchAt | `Date?` | `Date?` | `Long?` | `int64_t next_fetch_at` | | error | `string?` | `String?` | `String?` | `char* error` | | httpAuth | `HttpAuth?` | `HttpAuth?` | `HttpAuth?` | `HttpAuth* http_auth` | --- ## 5. Value Types vs Reference Types ### 5.1 Value Types (Copy on assign) All primitive types should be treated as **value types**: - UUID (string representation) - Date (Unix timestamp) - Boolean - Integer - Float ### 5.2 Reference Types (Pointer/Heap) Complex types should be **reference types**: - FeedItem (stored by ID reference) - FeedSubscription (stored by ID reference) - Enclosure (inline in FeedItem) - HttpAuth (inline in FeedSubscription) ### 5.3 Implementation Notes **Swift**: Use `struct` for value types, `class` for reference types **Kotlin**: Use `data class` for all models, pass by value **C**: Use struct with explicit memory management --- ## 6. Enumeration Types ### 6.1 ContentType ```typescript type ContentType = 'article' | 'audio' | 'video'; ``` | TypeScript | Swift | Kotlin | C | |------------|-------|--------|---| | `'article'` | `ContentType.article` | `ContentType.ARTICLE` | `CONTENT_TYPE_ARTICLE` | | `'audio'` | `ContentType.audio` | `ContentType.AUDIO` | `CONTENT_TYPE_AUDIO` | | `'video'` | `ContentType.video` | `ContentType.VIDEO` | `CONTENT_TYPE_VIDEO` | ### 6.2 Theme ```typescript type Theme = 'system' | 'light' | 'dark'; ``` | TypeScript | Swift | Kotlin | C | |------------|-------|--------|---| | `'system'` | `Theme.system` | `Theme.SYSTEM` | `THEME_SYSTEM` | | `'light'` | `Theme.light` | `Theme.LIGHT` | `THEME_LIGHT` | | `'dark'` | `Theme.dark` | `Theme.DARK` | `THEME_DARK` | ### 6.3 Privacy ```typescript type Privacy = 'public' | 'private' | 'anonymous'; ``` | TypeScript | Swift | Kotlin | C | |------------|-------|--------|---| | `'public'` | `Privacy.public` | `Privacy.PUBLIC` | `PRIVACY_PUBLIC` | | `'private'` | `Privacy.private` | `Privacy.PRIVATE` | `PRIVACY_PRIVATE` | | `'anonymous'` | `Privacy.anonymous` | `Privacy.ANONYMOUS` | `PRIVACY_ANONYMOUS` | ### 6.4 NotificationType ```typescript enum NotificationType { NEW_ARTICLE = 'NEW_ARTICLE', EPISODE_RELEASE = 'EPISODE_RELEASE', CUSTOM_ALERT = 'CUSTOM_ALERT', UPGRADE_PROMO = 'UPGRADE_PROMO' } ``` | TypeScript | Swift | Kotlin | C | |------------|-------|--------|---| | `NEW_ARTICLE` | `NotificationType.newArticle` | `NotificationType.NEW_ARTICLE` | `NOTIFICATION_TYPE_NEW_ARTICLE` | | `EPISODE_RELEASE` | `NotificationType.episodeRelease` | `NotificationType.EPISODE_RELEASE` | `NOTIFICATION_TYPE_EPISODE_RELEASE` | | `CUSTOM_ALERT` | `NotificationType.customAlert` | `NotificationType.CUSTOM_ALERT` | `NOTIFICATION_TYPE_CUSTOM_ALERT` | | `UPGRADE_PROMO` | `NotificationType.upgradePromo` | `NotificationType.UPGRADE_PROMO` | `NOTIFICATION_TYPE_UPGRADE_PROMO` | --- ## 7. Serialization/Deserialization ### 7.1 Date Handling All dates stored as **Unix timestamp (milliseconds)** in SQLite. Convert to/from platform-specific types: | Platform | Conversion | |----------|------------| | Swift | `Date(timeIntervalSince1970: timestamp / 1000)` | | Kotlin | `Date(timestamp)` | | C | `time_t` with `timestamp / 1000` | ### 7.2 JSON Fields Arrays and nested objects stored as JSON strings: ```sql -- Categories stored as JSON array categories = '["tech", "programming", "rust"]' -- Enclosure stored as JSON object enclosure = '{"url": "...", "type": "audio/mpeg", "length": 1234567}' ``` ### 7.3 UUID Handling Store UUIDs as strings in canonical format: ``` 550e8400-e29b-41d4-a716-446655440000 ``` Generate using platform UUID libraries: - Swift: `UUID().uuidString` - Kotlin: `UUID.randomUUID().toString()` - C: Use `uuid_generate()` from libuuid --- ## 8. Indexing Strategy ### 8.1 Primary Indexes - `subscriptions.id` (auto-created for PRIMARY KEY) - `feed_items.id` (auto-created for PRIMARY KEY) ### 8.2 Secondary Indexes | Table | Index | Purpose | |-------|-------|---------| | subscriptions | `idx_subscriptions_url` | URL lookups | | subscriptions | `idx_subscriptions_category` | Category filtering | | subscriptions | `idx_subscriptions_next_fetch` | Sync scheduling | | feed_items | `idx_feed_items_guid` | Deduplication | | feed_items | `idx_feed_items_subscription` | Feed-specific items | | feed_items | `idx_feed_items_published` | Sorting by date | | search_history | `idx_search_history_timestamp` | History sorting | ### 8.3 FTS Indexes | Virtual Table | Purpose | |---------------|---------| | `feed_items_fts` | Full-text search on articles | | `subscriptions_fts` | Full-text search on subscriptions | --- ## 9. Memory Considerations ### 9.1 Large Content Handling - `content` field can be very large (full article HTML) - Use lazy loading for content display - Consider compressing content with zlib for storage - Limit in-memory cache to N most recent items ### 9.2 Streaming for Large Result Sets Use cursor-based pagination: ```sql -- Instead of OFFSET SELECT * FROM feed_items WHERE subscription_id = ? ORDER BY published DESC LIMIT 20; -- Use last_published as cursor for next page ``` --- ## 10. Future Extensibility ### 10.1 Schema Versioning Add version table for migrations: ```sql CREATE TABLE schema_version ( version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL ); ``` ### 10.2 Adding New Fields - New optional fields: Add column with NULL default - New required fields: Use migration with DEFAULT values - Breaking changes: New table with shared ID ### 10.3 Feed Extensions Support for future feed types: - Podcast: Already supported via enclosure - Video feeds: Use content type detection - Media RSS: Extend enclosure model --- ## 11. Implementation Checklist - [ ] Define UUID type alias for each platform - [ ] Create Date conversion utilities - [ ] Implement FeedItem model (struct/class) - [ ] Implement FeedSubscription model - [ ] Implement SearchHistoryItem model - [ ] Create SQLite table definitions - [ ] Implement FTS5 triggers for feed_items - [ ] Implement FTS5 triggers for subscriptions - [ ] Add database indexes - [ ] Write serialization/deserialization code - [ ] Implement pagination helpers