Implement fiscal-style research MVP flows
Some checks failed
PR Checks / typecheck-and-build (push) Has been cancelled
Some checks failed
PR Checks / typecheck-and-build (push) Has been cancelled
This commit is contained in:
@@ -20,14 +20,23 @@ describe('sqlite schema compatibility bootstrap', () => {
|
||||
applyMigration(client, '0003_task_stage_event_timeline.sql');
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'category')).toBe(false);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'status')).toBe(false);
|
||||
expect(__dbInternals.hasColumn(client, 'holding', 'company_name')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_snapshot')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'research_journal_entry')).toBe(false);
|
||||
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'category')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'tags')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'status')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'priority')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'updated_at')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'last_reviewed_at')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'holding', 'company_name')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_snapshot')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_fact')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'research_journal_entry')).toBe(true);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
@@ -78,7 +78,11 @@ function ensureLocalSqliteSchema(client: Database) {
|
||||
if (hasTable(client, 'watchlist_item')) {
|
||||
const missingWatchlistColumns: Array<{ name: string; sql: string }> = [
|
||||
{ name: 'category', sql: 'ALTER TABLE `watchlist_item` ADD `category` text;' },
|
||||
{ name: 'tags', sql: 'ALTER TABLE `watchlist_item` ADD `tags` text;' }
|
||||
{ name: 'tags', sql: 'ALTER TABLE `watchlist_item` ADD `tags` text;' },
|
||||
{ name: 'status', sql: "ALTER TABLE `watchlist_item` ADD `status` text NOT NULL DEFAULT 'backlog';" },
|
||||
{ name: 'priority', sql: "ALTER TABLE `watchlist_item` ADD `priority` text NOT NULL DEFAULT 'medium';" },
|
||||
{ name: 'updated_at', sql: "ALTER TABLE `watchlist_item` ADD `updated_at` text NOT NULL DEFAULT '';" },
|
||||
{ name: 'last_reviewed_at', sql: 'ALTER TABLE `watchlist_item` ADD `last_reviewed_at` text;' }
|
||||
];
|
||||
|
||||
for (const column of missingWatchlistColumns) {
|
||||
@@ -86,11 +90,54 @@ function ensureLocalSqliteSchema(client: Database) {
|
||||
client.exec(column.sql);
|
||||
}
|
||||
}
|
||||
|
||||
client.exec(`
|
||||
UPDATE \`watchlist_item\`
|
||||
SET
|
||||
\`status\` = CASE
|
||||
WHEN \`status\` IS NULL OR TRIM(\`status\`) = '' THEN 'backlog'
|
||||
ELSE \`status\`
|
||||
END,
|
||||
\`priority\` = CASE
|
||||
WHEN \`priority\` IS NULL OR TRIM(\`priority\`) = '' THEN 'medium'
|
||||
ELSE \`priority\`
|
||||
END,
|
||||
\`updated_at\` = CASE
|
||||
WHEN \`updated_at\` IS NULL OR TRIM(\`updated_at\`) = '' THEN COALESCE(NULLIF(\`created_at\`, ''), CURRENT_TIMESTAMP)
|
||||
ELSE \`updated_at\`
|
||||
END;
|
||||
`);
|
||||
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `watchlist_user_updated_idx` ON `watchlist_item` (`user_id`, `updated_at`);');
|
||||
}
|
||||
|
||||
if (hasTable(client, 'holding') && !hasColumn(client, 'holding', 'company_name')) {
|
||||
client.exec('ALTER TABLE `holding` ADD `company_name` text;');
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'filing_taxonomy_snapshot')) {
|
||||
applySqlFile(client, '0005_financial_taxonomy_v3.sql');
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'research_journal_entry')) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`research_journal_entry\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`user_id\` text NOT NULL,
|
||||
\`ticker\` text NOT NULL,
|
||||
\`accession_number\` text,
|
||||
\`entry_type\` text NOT NULL,
|
||||
\`title\` text,
|
||||
\`body_markdown\` text NOT NULL,
|
||||
\`metadata\` text,
|
||||
\`created_at\` text NOT NULL,
|
||||
\`updated_at\` text NOT NULL,
|
||||
FOREIGN KEY (\`user_id\`) REFERENCES \`user\`(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`);
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_journal_ticker_idx` ON `research_journal_entry` (`user_id`, `ticker`, `created_at`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_journal_accession_idx` ON `research_journal_entry` (`user_id`, `accession_number`);');
|
||||
}
|
||||
}
|
||||
|
||||
export function getSqliteClient() {
|
||||
|
||||
@@ -27,6 +27,9 @@ type TaxonomyAssetType =
|
||||
|
||||
type TaxonomyParseStatus = 'ready' | 'partial' | 'failed';
|
||||
type TaxonomyMetricValidationStatus = 'not_run' | 'matched' | 'mismatch' | 'error';
|
||||
type CoverageStatus = 'backlog' | 'active' | 'watch' | 'archive';
|
||||
type CoveragePriority = 'low' | 'medium' | 'high';
|
||||
type ResearchJournalEntryType = 'note' | 'filing_note' | 'status_change';
|
||||
|
||||
type FilingAnalysis = {
|
||||
provider?: string;
|
||||
@@ -269,16 +272,22 @@ export const watchlistItem = sqliteTable('watchlist_item', {
|
||||
sector: text('sector'),
|
||||
category: text('category'),
|
||||
tags: text('tags', { mode: 'json' }).$type<string[]>(),
|
||||
created_at: text('created_at').notNull()
|
||||
status: text('status').$type<CoverageStatus>().notNull().default('backlog'),
|
||||
priority: text('priority').$type<CoveragePriority>().notNull().default('medium'),
|
||||
created_at: text('created_at').notNull(),
|
||||
updated_at: text('updated_at').notNull(),
|
||||
last_reviewed_at: text('last_reviewed_at')
|
||||
}, (table) => ({
|
||||
watchlistUserTickerUnique: uniqueIndex('watchlist_user_ticker_uidx').on(table.user_id, table.ticker),
|
||||
watchlistUserCreatedIndex: index('watchlist_user_created_idx').on(table.user_id, table.created_at)
|
||||
watchlistUserCreatedIndex: index('watchlist_user_created_idx').on(table.user_id, table.created_at),
|
||||
watchlistUserUpdatedIndex: index('watchlist_user_updated_idx').on(table.user_id, table.updated_at)
|
||||
}));
|
||||
|
||||
export const holding = sqliteTable('holding', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
ticker: text('ticker').notNull(),
|
||||
company_name: text('company_name'),
|
||||
shares: numeric('shares').notNull(),
|
||||
avg_cost: numeric('avg_cost').notNull(),
|
||||
current_price: numeric('current_price'),
|
||||
@@ -520,6 +529,22 @@ export const portfolioInsight = sqliteTable('portfolio_insight', {
|
||||
insightUserCreatedIndex: index('insight_user_created_idx').on(table.user_id, table.created_at)
|
||||
}));
|
||||
|
||||
export const researchJournalEntry = sqliteTable('research_journal_entry', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
ticker: text('ticker').notNull(),
|
||||
accession_number: text('accession_number'),
|
||||
entry_type: text('entry_type').$type<ResearchJournalEntryType>().notNull(),
|
||||
title: text('title'),
|
||||
body_markdown: text('body_markdown').notNull(),
|
||||
metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown> | null>(),
|
||||
created_at: text('created_at').notNull(),
|
||||
updated_at: text('updated_at').notNull()
|
||||
}, (table) => ({
|
||||
researchJournalTickerIndex: index('research_journal_ticker_idx').on(table.user_id, table.ticker, table.created_at),
|
||||
researchJournalAccessionIndex: index('research_journal_accession_idx').on(table.user_id, table.accession_number)
|
||||
}));
|
||||
|
||||
export const authSchema = {
|
||||
user,
|
||||
session,
|
||||
@@ -543,7 +568,8 @@ export const appSchema = {
|
||||
filingLink,
|
||||
taskRun,
|
||||
taskStageEvent,
|
||||
portfolioInsight
|
||||
portfolioInsight,
|
||||
researchJournalEntry
|
||||
};
|
||||
|
||||
export const schema = {
|
||||
|
||||
Reference in New Issue
Block a user