- Define core entities: FeedItem, FeedSubscription, SearchHistoryItem - Document entity relationships with diagram - Create database schema with SQLite and FTS5 - Provide type mapping guide for Swift/Kotlin/C - Define value types vs reference types - Document enumeration types for all platforms - Specify serialization/deserialization requirements - Outline indexing strategy for performance - Include memory considerations and future extensibility
17 KiB
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
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
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
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)
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<String>/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
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
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
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
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:
-- 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
contentfield 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:
-- 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:
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