React Native mobile application for SAPIENS news platform. Consolidated all previous history into single commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
232 lines
8.8 KiB
TypeScript
232 lines
8.8 KiB
TypeScript
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<typeof insertUserSchema>;
|
|
export type User = typeof users.$inferSelect;
|
|
export type InsertMediaOutlet = z.infer<typeof insertMediaOutletSchema>;
|
|
export type MediaOutlet = typeof mediaOutlets.$inferSelect;
|
|
export type InsertArticle = z.infer<typeof insertArticleSchema>;
|
|
export type Article = typeof articles.$inferSelect;
|
|
export type InsertComment = z.infer<typeof insertCommentSchema>;
|
|
export type Comment = typeof comments.$inferSelect;
|
|
export type InsertCommentReaction = z.infer<typeof insertCommentReactionSchema>;
|
|
export type CommentReaction = typeof commentReactions.$inferSelect;
|
|
export type InsertBookmark = z.infer<typeof insertBookmarkSchema>;
|
|
export type Bookmark = typeof bookmarks.$inferSelect;
|
|
export type InsertPredictionMarket = z.infer<typeof insertPredictionMarketSchema>;
|
|
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; |