Files
readlater/src/lib/db/schema.ts
Gemini Agent 96ece66204 Add article publish date to list and reader views
Extract publication dates from HTML meta tags when saving articles
and display them prominently in the article list and reader header.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:33:16 +00:00

117 lines
4.9 KiB
TypeScript

import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
// API Keys for authentication
export const apiKeys = sqliteTable("api_keys", {
id: text("id").primaryKey(),
name: text("name").notNull(), // e.g., "iPhone Shortcut", "Mac Safari"
key: text("key").notNull().unique(),
lastUsed: integer("last_used", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// Folders for organizing articles
export const folders = sqliteTable("folders", {
id: text("id").primaryKey(),
name: text("name").notNull(),
color: text("color").default("#3b82f6"), // Accent color
icon: text("icon").default("folder"), // Lucide icon name
parentId: text("parent_id"), // For nested folders
sortOrder: integer("sort_order").default(0),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// Articles
export const articles = sqliteTable("articles", {
id: text("id").primaryKey(),
url: text("url").notNull(),
title: text("title").notNull(),
author: text("author"),
siteName: text("site_name"),
excerpt: text("excerpt"),
content: text("content").notNull(), // HTML content
textContent: text("text_content").notNull(), // Plain text for TTS
leadImage: text("lead_image"),
wordCount: integer("word_count").default(0),
readingProgress: integer("reading_progress").default(0), // 0-100
readingTimeSeconds: integer("reading_time_seconds").default(0), // Total time spent reading
isFavorite: integer("is_favorite", { mode: "boolean" }).default(false),
isArchived: integer("is_archived", { mode: "boolean" }).default(false),
folderId: text("folder_id"), // Link to folder
tags: text("tags").default("[]"), // JSON array of tags
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
readAt: integer("read_at", { mode: "timestamp" }),
finishedAt: integer("finished_at", { mode: "timestamp" }), // When reading was completed
publishedAt: integer("published_at", { mode: "timestamp" }), // Original article publish date
});
// Highlights and notes
export const highlights = sqliteTable("highlights", {
id: text("id").primaryKey(),
articleId: text("article_id").notNull(),
text: text("text").notNull(), // Highlighted text
note: text("note"), // Optional note
color: text("color").default("#fbbf24"), // Highlight color
startOffset: integer("start_offset"), // Position in textContent
endOffset: integer("end_offset"),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// Daily reading stats
export const readingStats = sqliteTable("reading_stats", {
id: text("id").primaryKey(),
date: text("date").notNull().unique(), // YYYY-MM-DD format
articlesRead: integer("articles_read").default(0),
articlesAdded: integer("articles_added").default(0),
wordsRead: integer("words_read").default(0),
timeSpentSeconds: integer("time_spent_seconds").default(0),
streak: integer("streak").default(0), // Consecutive days
});
// Reading goals
export const readingGoals = sqliteTable("reading_goals", {
id: text("id").primaryKey(),
type: text("type").notNull(), // "daily", "weekly", "monthly"
metric: text("metric").notNull(), // "articles", "words", "time"
target: integer("target").notNull(),
isActive: integer("is_active", { mode: "boolean" }).default(true),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// App settings
export const settings = sqliteTable("settings", {
key: text("key").primaryKey(),
value: text("value").notNull(), // JSON encoded value
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// Email inbox config (for email-to-save)
export const emailConfig = sqliteTable("email_config", {
id: text("id").primaryKey(),
inboxEmail: text("inbox_email").unique(), // e.g., save-abc123@readlater.example.com
isActive: integer("is_active", { mode: "boolean" }).default(true),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
// Types
export type ApiKey = typeof apiKeys.$inferSelect;
export type NewApiKey = typeof apiKeys.$inferInsert;
export type Folder = typeof folders.$inferSelect;
export type NewFolder = typeof folders.$inferInsert;
export type Article = typeof articles.$inferSelect;
export type NewArticle = typeof articles.$inferInsert;
export type Highlight = typeof highlights.$inferSelect;
export type NewHighlight = typeof highlights.$inferInsert;
export type ReadingStats = typeof readingStats.$inferSelect;
export type NewReadingStats = typeof readingStats.$inferInsert;
export type ReadingGoal = typeof readingGoals.$inferSelect;
export type NewReadingGoal = typeof readingGoals.$inferInsert;
export type Setting = typeof settings.$inferSelect;
export type NewSetting = typeof settings.$inferInsert;