Files
Neon-Desk/lib/server/db/schema.ts

368 lines
14 KiB
TypeScript

import {
index,
integer,
numeric,
sqliteTable,
text,
uniqueIndex
} from 'drizzle-orm/sqlite-core';
type FilingMetrics = {
revenue: number | null;
netIncome: number | null;
totalAssets: number | null;
cash: number | null;
debt: number | null;
};
type FilingAnalysis = {
provider?: string;
model?: string;
text?: string;
legacyInsights?: string;
extraction?: {
summary: string;
keyPoints: string[];
redFlags: string[];
followUpQuestions: string[];
portfolioSignals: string[];
segmentSpecificData: string[];
geographicRevenueBreakdown: string[];
companySpecificData: string[];
secApiCrossChecks: string[];
confidence: number;
};
extractionMeta?: {
provider: string;
model: string;
source: 'primary_document' | 'metadata_fallback';
generatedAt: string;
};
};
type FinancialStatementKind = 'income' | 'balance' | 'cash_flow' | 'equity' | 'comprehensive_income';
type FilingStatementPeriod = {
id: string;
filingId: number;
accessionNumber: string;
filingDate: string;
periodEnd: string | null;
filingType: '10-K' | '10-Q';
periodLabel: string;
};
type StatementValuesByPeriod = Record<string, number | null>;
type FilingFaithfulStatementSnapshotRow = {
key: string;
label: string;
concept: string | null;
order: number;
depth: number;
isSubtotal: boolean;
values: StatementValuesByPeriod;
};
type StandardizedStatementSnapshotRow = {
key: string;
label: string;
concept: string;
category: string;
sourceConcepts: string[];
values: StatementValuesByPeriod;
};
type DimensionStatementSnapshotRow = {
rowKey: string;
concept: string | null;
periodId: string;
axis: string;
member: string;
value: number | null;
unit: string | null;
};
type FilingStatementBundle = {
periods: FilingStatementPeriod[];
statements: Record<FinancialStatementKind, FilingFaithfulStatementSnapshotRow[]>;
};
type StandardizedStatementBundle = {
periods: FilingStatementPeriod[];
statements: Record<FinancialStatementKind, StandardizedStatementSnapshotRow[]>;
};
type DimensionStatementBundle = {
statements: Record<FinancialStatementKind, DimensionStatementSnapshotRow[]>;
};
const authDateColumn = {
mode: 'timestamp_ms'
} as const;
export const user = sqliteTable('user', {
id: text('id').primaryKey().notNull(),
name: text('name').notNull(),
email: text('email').notNull(),
emailVerified: integer('emailVerified', { mode: 'boolean' }).notNull().default(false),
image: text('image'),
createdAt: integer('createdAt', authDateColumn).notNull(),
updatedAt: integer('updatedAt', authDateColumn).notNull(),
role: text('role'),
banned: integer('banned', { mode: 'boolean' }).default(false),
banReason: text('banReason'),
banExpires: integer('banExpires', authDateColumn)
}, (table) => ({
userEmailUnique: uniqueIndex('user_email_uidx').on(table.email)
}));
export const organization = sqliteTable('organization', {
id: text('id').primaryKey().notNull(),
name: text('name').notNull(),
slug: text('slug').notNull(),
logo: text('logo'),
createdAt: integer('createdAt', authDateColumn).notNull(),
metadata: text('metadata')
}, (table) => ({
organizationSlugUnique: uniqueIndex('organization_slug_uidx').on(table.slug)
}));
export const session = sqliteTable('session', {
id: text('id').primaryKey().notNull(),
expiresAt: integer('expiresAt', authDateColumn).notNull(),
token: text('token').notNull(),
createdAt: integer('createdAt', authDateColumn).notNull(),
updatedAt: integer('updatedAt', authDateColumn).notNull(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
impersonatedBy: text('impersonatedBy'),
activeOrganizationId: text('activeOrganizationId')
}, (table) => ({
sessionTokenUnique: uniqueIndex('session_token_uidx').on(table.token),
sessionUserIdIndex: index('session_userId_idx').on(table.userId)
}));
export const account = sqliteTable('account', {
id: text('id').primaryKey().notNull(),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
accessTokenExpiresAt: integer('accessTokenExpiresAt', authDateColumn),
refreshTokenExpiresAt: integer('refreshTokenExpiresAt', authDateColumn),
scope: text('scope'),
password: text('password'),
createdAt: integer('createdAt', authDateColumn).notNull(),
updatedAt: integer('updatedAt', authDateColumn).notNull()
}, (table) => ({
accountUserIdIndex: index('account_userId_idx').on(table.userId)
}));
export const verification = sqliteTable('verification', {
id: text('id').primaryKey().notNull(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expiresAt', authDateColumn).notNull(),
createdAt: integer('createdAt', authDateColumn).notNull(),
updatedAt: integer('updatedAt', authDateColumn).notNull()
}, (table) => ({
verificationIdentifierIndex: index('verification_identifier_idx').on(table.identifier)
}));
export const member = sqliteTable('member', {
id: text('id').primaryKey().notNull(),
organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
role: text('role').notNull().default('member'),
createdAt: integer('createdAt', authDateColumn).notNull()
}, (table) => ({
memberOrganizationIdIndex: index('member_organizationId_idx').on(table.organizationId),
memberUserIdIndex: index('member_userId_idx').on(table.userId)
}));
export const invitation = sqliteTable('invitation', {
id: text('id').primaryKey().notNull(),
organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }),
email: text('email').notNull(),
role: text('role'),
status: text('status').notNull().default('pending'),
expiresAt: integer('expiresAt', authDateColumn).notNull(),
createdAt: integer('createdAt', authDateColumn).notNull(),
inviterId: text('inviterId').notNull().references(() => user.id, { onDelete: 'cascade' })
}, (table) => ({
invitationOrganizationIdIndex: index('invitation_organizationId_idx').on(table.organizationId),
invitationEmailIndex: index('invitation_email_idx').on(table.email)
}));
export const watchlistItem = sqliteTable('watchlist_item', {
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').notNull(),
sector: text('sector'),
created_at: text('created_at').notNull()
}, (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)
}));
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(),
shares: numeric('shares').notNull(),
avg_cost: numeric('avg_cost').notNull(),
current_price: numeric('current_price'),
market_value: numeric('market_value').notNull(),
gain_loss: numeric('gain_loss').notNull(),
gain_loss_pct: numeric('gain_loss_pct').notNull(),
last_price_at: text('last_price_at'),
created_at: text('created_at').notNull(),
updated_at: text('updated_at').notNull()
}, (table) => ({
holdingUserTickerUnique: uniqueIndex('holding_user_ticker_uidx').on(table.user_id, table.ticker),
holdingUserIndex: index('holding_user_idx').on(table.user_id)
}));
export const filing = sqliteTable('filing', {
id: integer('id').primaryKey({ autoIncrement: true }),
ticker: text('ticker').notNull(),
filing_type: text('filing_type').$type<'10-K' | '10-Q' | '8-K'>().notNull(),
filing_date: text('filing_date').notNull(),
accession_number: text('accession_number').notNull(),
cik: text('cik').notNull(),
company_name: text('company_name').notNull(),
filing_url: text('filing_url'),
submission_url: text('submission_url'),
primary_document: text('primary_document'),
metrics: text('metrics', { mode: 'json' }).$type<FilingMetrics | null>(),
analysis: text('analysis', { mode: 'json' }).$type<FilingAnalysis | null>(),
created_at: text('created_at').notNull(),
updated_at: text('updated_at').notNull()
}, (table) => ({
filingAccessionUnique: uniqueIndex('filing_accession_uidx').on(table.accession_number),
filingTickerDateIndex: index('filing_ticker_date_idx').on(table.ticker, table.filing_date),
filingDateIndex: index('filing_date_idx').on(table.filing_date)
}));
export const filingStatementSnapshot = sqliteTable('filing_statement_snapshot', {
id: integer('id').primaryKey({ autoIncrement: true }),
filing_id: integer('filing_id').notNull().references(() => filing.id, { onDelete: 'cascade' }),
ticker: text('ticker').notNull(),
filing_date: text('filing_date').notNull(),
filing_type: text('filing_type').$type<'10-K' | '10-Q'>().notNull(),
period_end: text('period_end'),
statement_bundle: text('statement_bundle', { mode: 'json' }).$type<FilingStatementBundle | null>(),
standardized_bundle: text('standardized_bundle', { mode: 'json' }).$type<StandardizedStatementBundle | null>(),
dimension_bundle: text('dimension_bundle', { mode: 'json' }).$type<DimensionStatementBundle | null>(),
parse_status: text('parse_status').$type<'ready' | 'partial' | 'failed'>().notNull(),
parse_error: text('parse_error'),
source: text('source').$type<'sec_filing_summary' | 'xbrl_instance' | 'companyfacts_fallback'>().notNull(),
created_at: text('created_at').notNull(),
updated_at: text('updated_at').notNull()
}, (table) => ({
filingStatementFilingUnique: uniqueIndex('filing_stmt_filing_uidx').on(table.filing_id),
filingStatementTickerDateIndex: index('filing_stmt_ticker_date_idx').on(table.ticker, table.filing_date),
filingStatementDateIndex: index('filing_stmt_date_idx').on(table.filing_date),
filingStatementStatusIndex: index('filing_stmt_status_idx').on(table.parse_status)
}));
export const filingLink = sqliteTable('filing_link', {
id: integer('id').primaryKey({ autoIncrement: true }),
filing_id: integer('filing_id').notNull().references(() => filing.id, { onDelete: 'cascade' }),
link_type: text('link_type').notNull(),
url: text('url').notNull(),
source: text('source').notNull().default('sec'),
created_at: text('created_at').notNull()
}, (table) => ({
filingLinkUnique: uniqueIndex('filing_link_unique_uidx').on(table.filing_id, table.url),
filingLinkFilingIndex: index('filing_link_filing_idx').on(table.filing_id)
}));
export const taskRun = sqliteTable('task_run', {
id: text('id').primaryKey().notNull(),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
task_type: text('task_type').$type<'sync_filings' | 'refresh_prices' | 'analyze_filing' | 'portfolio_insights'>().notNull(),
status: text('status').$type<'queued' | 'running' | 'completed' | 'failed'>().notNull(),
stage: text('stage').notNull(),
stage_detail: text('stage_detail'),
resource_key: text('resource_key'),
notification_read_at: text('notification_read_at'),
notification_silenced_at: text('notification_silenced_at'),
priority: integer('priority').notNull(),
payload: text('payload', { mode: 'json' }).$type<Record<string, unknown>>().notNull(),
result: text('result', { mode: 'json' }).$type<Record<string, unknown> | null>(),
error: text('error'),
attempts: integer('attempts').notNull(),
max_attempts: integer('max_attempts').notNull(),
workflow_run_id: text('workflow_run_id'),
created_at: text('created_at').notNull(),
updated_at: text('updated_at').notNull(),
finished_at: text('finished_at')
}, (table) => ({
taskUserCreatedIndex: index('task_user_created_idx').on(table.user_id, table.created_at),
taskStatusIndex: index('task_status_idx').on(table.status),
taskUserResourceStatusIndex: index('task_user_resource_status_idx').on(
table.user_id,
table.task_type,
table.resource_key,
table.status,
table.created_at
),
taskWorkflowRunUnique: uniqueIndex('task_workflow_run_uidx').on(table.workflow_run_id)
}));
export const taskStageEvent = sqliteTable('task_stage_event', {
id: integer('id').primaryKey({ autoIncrement: true }),
task_id: text('task_id').notNull().references(() => taskRun.id, { onDelete: 'cascade' }),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stage: text('stage').notNull(),
stage_detail: text('stage_detail'),
status: text('status').$type<'queued' | 'running' | 'completed' | 'failed'>().notNull(),
created_at: text('created_at').notNull()
}, (table) => ({
taskStageEventTaskCreatedIndex: index('task_stage_event_task_created_idx').on(table.task_id, table.created_at),
taskStageEventUserCreatedIndex: index('task_stage_event_user_created_idx').on(table.user_id, table.created_at)
}));
export const portfolioInsight = sqliteTable('portfolio_insight', {
id: integer('id').primaryKey({ autoIncrement: true }),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
provider: text('provider').notNull(),
model: text('model').notNull(),
content: text('content').notNull(),
created_at: text('created_at').notNull()
}, (table) => ({
insightUserCreatedIndex: index('insight_user_created_idx').on(table.user_id, table.created_at)
}));
export const authSchema = {
user,
session,
account,
verification,
organization,
member,
invitation
};
export const appSchema = {
watchlistItem,
holding,
filing,
filingStatementSnapshot,
filingLink,
taskRun,
taskStageEvent,
portfolioInsight
};
export const schema = {
...authSchema,
...appSchema
};