Edo Limburg c79eb6d76d Add CLI entry point, RSS content extraction, and image support
Features:
- Add CLI with commands: start, add, remove, list, fetch, status, items
- Auto-detect RSS format when adding feeds
- Auto-run database migrations on startup
- Extract full HTML content from RSS description field (NOS-style feeds)
- Extract image URLs from RSS enclosure tags
- Display images in terminal output with emoji
- Include imageUrl in JSON formatter output

Database:
- Add image_url column to feed_items table
- Update storage layer to persist imageUrl field

Tests:
- Add 10 CLI integration tests
- Add 3 RSS parser tests for image/content extraction
- Add 2 storage tests for imageUrl persistence

Dependencies:
- Add commander for CLI framework

All 144 tests passing
2026-05-05 23:05:30 +02:00

83 lines
3.0 KiB
TypeScript

/**
* Database schema migrations.
* Creates tables idempotently.
*/
import type { Kysely } from 'kysely';
import type { Database } from './database.js';
export async function migrate(db: Kysely<Database>): Promise<void> {
// Create feed_items table
await db.schema
.createTable('feed_items')
.ifNotExists()
.addColumn('id', 'varchar(64)', (col) => col.primaryKey())
.addColumn('source', 'varchar(2048)', (col) => col.notNull())
.addColumn('title', 'varchar(512)', (col) => col.notNull())
.addColumn('url', 'varchar(2048)', (col) => col.notNull())
.addColumn('published_at', 'varchar(32)', (col) => col.notNull())
.addColumn('content', 'text')
.addColumn('summary', 'text')
.addColumn('image_url', 'varchar(2048)')
.addColumn('created_at', 'varchar(32)', (col) => col.notNull().defaultTo('CURRENT_TIMESTAMP'))
.execute();
// Create indexes for feed_items
await db.schema
.createIndex('idx_feed_items_source')
.ifNotExists()
.on('feed_items')
.column('source')
.execute();
await db.schema
.createIndex('idx_feed_items_published')
.ifNotExists()
.on('feed_items')
.column('published_at')
.execute();
// Create seen_ids table
await db.schema
.createTable('seen_ids')
.ifNotExists()
.addColumn('id', 'varchar(64)', (col) => col.primaryKey())
.addColumn('seen_at', 'varchar(32)', (col) => col.notNull().defaultTo('CURRENT_TIMESTAMP'))
.execute();
// Create feed_sources table
await db.schema
.createTable('feed_sources')
.ifNotExists()
.addColumn('id', 'varchar(64)', (col) => col.primaryKey())
.addColumn('url', 'varchar(2048)', (col) => col.notNull().unique())
.addColumn('name', 'varchar(256)')
.addColumn('format', 'varchar(10)', (col) => col.notNull())
.addColumn('poll_interval_ms', 'integer', (col) => col.notNull())
.addColumn('is_active', 'boolean', (col) => col.notNull().defaultTo(true))
.addColumn('last_fetched_at', 'varchar(32)')
.addColumn('last_success_at', 'varchar(32)')
.addColumn('consecutive_failures', 'integer', (col) => col.notNull().defaultTo(0))
.addColumn('created_at', 'varchar(32)', (col) => col.notNull().defaultTo('CURRENT_TIMESTAMP'))
.addColumn('updated_at', 'varchar(32)', (col) => col.notNull().defaultTo('CURRENT_TIMESTAMP'))
.execute();
// Create index on is_active for quick filtering
await db.schema
.createIndex('idx_feed_sources_active')
.ifNotExists()
.on('feed_sources')
.column('is_active')
.execute();
}
export async function reset(db: Kysely<Database>): Promise<void> {
// Drop tables (for testing)
await db.schema.dropIndex('idx_feed_sources_active').ifExists().execute();
await db.schema.dropTable('feed_sources').ifExists().execute();
await db.schema.dropTable('seen_ids').ifExists().execute();
await db.schema.dropIndex('idx_feed_items_published').ifExists().execute();
await db.schema.dropIndex('idx_feed_items_source').ifExists().execute();
await db.schema.dropTable('feed_items').ifExists().execute();
}