import { sql } from "drizzle-orm"; import { pgTable, text, varchar, timestamp, integer, pgEnum, foreignKey, uniqueIndex, index } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; // Enum for reaction types export const reactionTypeEnum = pgEnum('reaction_type', ['like', 'dislike']); // User schema export const users = pgTable("users", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), username: text("username").notNull().unique(), password: text("password").notNull(), preferredLanguage: text("preferred_language").default("en"), avatar: text("avatar"), }); // Media outlet schema export const mediaOutlets = pgTable("media_outlets", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), name: text("name").notNull(), description: text("description").notNull(), category: text("category").notNull(), // "people", "topics", "companies" focusSubject: text("focus_subject").notNull(), // "jacob", "ala", "ai", "crypto", etc. avatar: text("avatar"), profileImage: text("profile_image"), bio: text("bio").notNull(), fullBio: text("full_bio").array(), wikiProfile: text("wiki_profile"), // Detailed Wikipedia-style profile in HTML/Markdown format }); // Article schema - extended for web scraping export const articles = pgTable("articles", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), outletId: varchar("outlet_id").notNull().references(() => mediaOutlets.id), title: text("title").notNull(), summary: text("summary").notNull(), body: text("body").notNull(), thumbnail: text("thumbnail").notNull(), publishedAt: timestamp("published_at").notNull(), tags: text("tags").array(), viewCount: integer("view_count").notNull().default(0), // Scraped article fields sourceUrl: text("source_url"), // Original URL that was scraped author: text("author"), // Article author from scraped content originalImageUrl: text("original_image_url"), // Original image URL before processing scrapedAt: timestamp("scraped_at").default(sql`NOW()`), // When this article was scraped isScraped: integer("is_scraped").notNull().default(0), // 1 if scraped, 0 if manually created }); // Comments schema for YouTube-style commenting system export const comments = pgTable("comments", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), articleId: varchar("article_id").notNull().references(() => articles.id), content: text("content").notNull(), nickname: text("nickname").notNull(), avatar: text("avatar"), // Optional avatar URL or emoji parentId: varchar("parent_id"), // Parent comment ID (nullable for top-level comments) likesCount: integer("likes_count").notNull().default(0), dislikesCount: integer("dislikes_count").notNull().default(0), repliesCount: integer("replies_count").notNull().default(0), createdAt: timestamp("created_at").notNull().default(sql`NOW()`), updatedAt: timestamp("updated_at").notNull().default(sql`NOW()`), }, (table) => ({ // Performance indexes articleParentCreatedIdx: index("comments_article_parent_created_idx").on( table.articleId, table.parentId, table.createdAt ), })); // Comment reactions (likes/dislikes) with deduplication constraint export const commentReactions = pgTable("comment_reactions", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), commentId: varchar("comment_id").notNull().references(() => comments.id, { onDelete: 'cascade' }), reactionType: text("reaction_type").notNull(), // Keep as text to avoid migration issues userIdentifier: text("user_identifier").notNull(), // Session ID, IP hash, or similar createdAt: timestamp("created_at").notNull().default(sql`NOW()`), }, (table) => ({ // Unique constraint to prevent duplicate reactions from same user uniqueUserReaction: uniqueIndex("unique_user_reaction_per_comment_idx").on( table.commentId, table.userIdentifier ), })); // Bookmarks schema for saving articles export const bookmarks = pgTable("bookmarks", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), articleId: varchar("article_id").notNull().references(() => articles.id, { onDelete: 'cascade' }), userIdentifier: text("user_identifier").notNull(), // Session ID, IP hash, or similar createdAt: timestamp("created_at").notNull().default(sql`NOW()`), }, (table) => ({ // Unique constraint to prevent duplicate bookmarks from same user uniqueUserBookmark: uniqueIndex("unique_user_bookmark_per_article_idx").on( table.articleId, table.userIdentifier ), })); // Prediction Markets schema for article-related betting events export const predictionMarkets = pgTable("prediction_markets", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), articleId: varchar("article_id").notNull().references(() => articles.id), question: text("question").notNull(), description: text("description"), marketType: text("market_type").notNull().default("binary"), // "binary" (yes/no) or "multiple_choice" // For binary markets yesPrice: integer("yes_price"), // Price in cents (0-100) noPrice: integer("no_price"), // Price in cents (0-100) // For multiple choice markets - JSON array of options // [{name: "Option A", price: 60, image: "/url"}, ...] options: text("options"), // JSON string totalVolume: integer("total_volume").notNull().default(0), // Total trading volume in dollars endDate: timestamp("end_date").notNull(), category: text("category"), isLive: integer("is_live").notNull().default(0), isResolved: integer("is_resolved").notNull().default(0), createdAt: timestamp("created_at").notNull().default(sql`NOW()`), }, (table) => ({ articleIdx: index("prediction_markets_article_idx").on(table.articleId), })); // Insert schemas export const insertUserSchema = createInsertSchema(users).pick({ username: true, password: true, preferredLanguage: true, avatar: true, }); export const insertMediaOutletSchema = createInsertSchema(mediaOutlets).pick({ name: true, description: true, category: true, focusSubject: true, avatar: true, profileImage: true, bio: true, fullBio: true, wikiProfile: true, }); export const insertArticleSchema = createInsertSchema(articles).pick({ outletId: true, title: true, summary: true, body: true, thumbnail: true, publishedAt: true, tags: true, viewCount: true, sourceUrl: true, author: true, originalImageUrl: true, scrapedAt: true, isScraped: true, }).extend({ publishedAt: z.preprocess((val) => { if (typeof val === 'string') return new Date(val); if (val instanceof Date) return val; return new Date(); }, z.date()), }); export const insertCommentSchema = createInsertSchema(comments).pick({ articleId: true, content: true, nickname: true, avatar: true, parentId: true, }); export const insertCommentReactionSchema = createInsertSchema(commentReactions).pick({ commentId: true, reactionType: true, userIdentifier: true, }).extend({ reactionType: z.enum(["like", "dislike"]), }); export const insertBookmarkSchema = createInsertSchema(bookmarks).pick({ articleId: true, userIdentifier: true, }); export const insertPredictionMarketSchema = createInsertSchema(predictionMarkets).pick({ articleId: true, question: true, description: true, marketType: true, yesPrice: true, noPrice: true, options: true, totalVolume: true, endDate: true, category: true, isLive: true, isResolved: true, }); // Types export type InsertUser = z.infer; export type User = typeof users.$inferSelect; export type InsertMediaOutlet = z.infer; export type MediaOutlet = typeof mediaOutlets.$inferSelect; export type InsertArticle = z.infer; export type Article = typeof articles.$inferSelect; export type InsertComment = z.infer; export type Comment = typeof comments.$inferSelect; export type InsertCommentReaction = z.infer; export type CommentReaction = typeof commentReactions.$inferSelect; export type InsertBookmark = z.infer; export type Bookmark = typeof bookmarks.$inferSelect; export type InsertPredictionMarket = z.infer; export type PredictionMarket = typeof predictionMarkets.$inferSelect; // Enums for categories export const CATEGORIES = ["people", "topics", "companies"] as const; export const FOCUS_SUBJECTS = ["jacob", "ala", "ai", "crypto", "polychain", "dcg"] as const; export const LANGUAGES = [ { code: "en", name: "English" }, { code: "ko", name: "한국어" }, { code: "ja", name: "日本語" }, { code: "zh_cn", name: "简体中文" }, { code: "zh_tw", name: "繁體中文" }, { code: "de", name: "Deutsch" }, { code: "fr", name: "Français" }, { code: "es", name: "Español" }, { code: "it", name: "Italiano" }, ] as const;