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:
@@ -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
|
||||
Reference in New Issue
Block a user