Design shared data models for native platforms

- 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
This commit is contained in:
2026-03-29 14:02:34 -04:00
parent 5f4d5a52e8
commit af87f9f571

View File

@@ -0,0 +1,506 @@
# 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<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
```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