212 lines
5.5 KiB
TypeScript
212 lines
5.5 KiB
TypeScript
/**
|
|
* IndexedDB database for storing analysis reports
|
|
* Uses Dexie.js for a cleaner API
|
|
*/
|
|
|
|
import Dexie, { type Table } from "dexie";
|
|
import type { AnalysisResponse } from "./types";
|
|
import { normalizeLanguage } from "./report-utils";
|
|
|
|
// Saved report interface
|
|
export interface SavedReport {
|
|
id?: number; // Auto-generated primary key
|
|
ticker: string; // Stock ticker symbol
|
|
market_type: "us" | "twse" | "tpex"; // Market type
|
|
analysis_date: string; // Analysis date (YYYY-MM-DD)
|
|
saved_at: Date; // Save timestamp
|
|
task_id?: string; // Original task ID
|
|
result: AnalysisResponse; // Full analysis result
|
|
language?: "en" | "zh-TW"; // Language of the report (for filtering)
|
|
cloud_id?: string; // Corresponding cloud report ID (for sync tracking)
|
|
pending_sync?: boolean; // Whether report is waiting to be synced to cloud
|
|
}
|
|
|
|
// Database class extending Dexie
|
|
class ReportsDatabase extends Dexie {
|
|
reports!: Table<SavedReport>;
|
|
|
|
constructor() {
|
|
super("TradingAgentsReports");
|
|
// Version 1: Original schema
|
|
this.version(1).stores({
|
|
reports: "++id, ticker, market_type, analysis_date, saved_at",
|
|
});
|
|
// Version 2: Added language field for filtering by UI language
|
|
this.version(2).stores({
|
|
reports: "++id, ticker, market_type, analysis_date, saved_at, language",
|
|
});
|
|
// Version 3: Added cloud_id and pending_sync for sync tracking
|
|
this.version(3).stores({
|
|
reports:
|
|
"++id, ticker, market_type, analysis_date, saved_at, language, cloud_id, pending_sync",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Database singleton instance
|
|
const db = new ReportsDatabase();
|
|
|
|
/**
|
|
* Save a report to the database
|
|
*/
|
|
export async function saveReport(
|
|
ticker: string,
|
|
market_type: "us" | "twse" | "tpex",
|
|
analysis_date: string,
|
|
result: AnalysisResponse,
|
|
task_id?: string,
|
|
language?: "en" | "zh-TW",
|
|
): Promise<number> {
|
|
const report: SavedReport = {
|
|
ticker,
|
|
market_type,
|
|
analysis_date,
|
|
saved_at: new Date(),
|
|
task_id,
|
|
result,
|
|
language,
|
|
};
|
|
|
|
return await db.reports.add(report);
|
|
}
|
|
|
|
/**
|
|
* Get all reports by market type
|
|
*/
|
|
export async function getReportsByMarketType(
|
|
market_type: "us" | "twse" | "tpex",
|
|
): Promise<SavedReport[]> {
|
|
return await db.reports
|
|
.where("market_type")
|
|
.equals(market_type)
|
|
.reverse()
|
|
.sortBy("saved_at");
|
|
}
|
|
|
|
/**
|
|
* Get all saved reports, sorted by saved_at descending
|
|
*/
|
|
export async function getAllReports(): Promise<SavedReport[]> {
|
|
return await db.reports.orderBy("saved_at").reverse().toArray();
|
|
}
|
|
|
|
/**
|
|
* Get a single report by ID
|
|
*/
|
|
export async function getReportById(
|
|
id: number,
|
|
): Promise<SavedReport | undefined> {
|
|
return await db.reports.get(id);
|
|
}
|
|
|
|
/**
|
|
* Delete a report by ID
|
|
*/
|
|
export async function deleteReport(id: number): Promise<void> {
|
|
await db.reports.delete(id);
|
|
}
|
|
|
|
/**
|
|
* Delete multiple reports by IDs
|
|
*/
|
|
export async function deleteReports(ids: number[]): Promise<void> {
|
|
await db.reports.bulkDelete(ids);
|
|
}
|
|
|
|
/**
|
|
* Get report count by market type
|
|
*/
|
|
export async function getReportCountByMarketType(): Promise<{
|
|
us: number;
|
|
twse: number;
|
|
tpex: number;
|
|
}> {
|
|
const [us, twse, tpex] = await Promise.all([
|
|
db.reports.where("market_type").equals("us").count(),
|
|
db.reports.where("market_type").equals("twse").count(),
|
|
db.reports.where("market_type").equals("tpex").count(),
|
|
]);
|
|
|
|
return { us, twse, tpex };
|
|
}
|
|
|
|
/**
|
|
* Check if a report with the same signature already exists.
|
|
* Supports optional market_type and language for precise matching.
|
|
*/
|
|
export async function checkDuplicateReport(
|
|
ticker: string,
|
|
analysis_date: string,
|
|
market_type?: "us" | "twse" | "tpex",
|
|
language?: "en" | "zh-TW",
|
|
): Promise<SavedReport | undefined> {
|
|
const normalizedLang = normalizeLanguage(language);
|
|
return await db.reports
|
|
.where("ticker")
|
|
.equals(ticker)
|
|
.and((report) => {
|
|
if (report.analysis_date !== analysis_date) return false;
|
|
if (market_type && report.market_type !== market_type) return false;
|
|
if (normalizeLanguage(report.language) !== normalizedLang) return false;
|
|
return true;
|
|
})
|
|
.first();
|
|
}
|
|
|
|
/**
|
|
* Check if a report exists by ticker, date, market type, and language
|
|
* Used for bidirectional sync to prevent duplicates.
|
|
* Language is normalized so null/undefined matches "zh-TW".
|
|
*/
|
|
export async function findExistingReport(
|
|
ticker: string,
|
|
analysis_date: string,
|
|
market_type: "us" | "twse" | "tpex",
|
|
language?: "en" | "zh-TW",
|
|
): Promise<SavedReport | undefined> {
|
|
const normalizedLang = normalizeLanguage(language);
|
|
return await db.reports
|
|
.where("ticker")
|
|
.equals(ticker)
|
|
.and(
|
|
(report) =>
|
|
report.analysis_date === analysis_date &&
|
|
report.market_type === market_type &&
|
|
normalizeLanguage(report.language) === normalizedLang
|
|
)
|
|
.first();
|
|
}
|
|
|
|
/**
|
|
* Bulk save reports to the database (for syncing from cloud)
|
|
* Skips reports that already exist locally
|
|
*/
|
|
export async function bulkSaveReports(
|
|
reports: Omit<SavedReport, "id">[]
|
|
): Promise<number> {
|
|
let savedCount = 0;
|
|
for (const report of reports) {
|
|
const existing = await findExistingReport(
|
|
report.ticker,
|
|
report.analysis_date,
|
|
report.market_type,
|
|
report.language
|
|
);
|
|
if (!existing) {
|
|
await db.reports.add(report as SavedReport);
|
|
savedCount++;
|
|
}
|
|
}
|
|
return savedCount;
|
|
}
|
|
|
|
/**
|
|
* Clear all reports from the database (for logout)
|
|
*/
|
|
export async function clearAllReports(): Promise<void> {
|
|
await db.reports.clear();
|
|
}
|
|
|
|
// Export the db instance for advanced usage
|
|
export { db };
|