/** * Drizzle ORM Schema for the Plant Disease Knowledge Base. * * Uses Turso (libSQL) with SQLite dialect. * Arrays (symptoms, causes, treatment, prevention, lookalike_ids) * are stored as JSON text columns and typed via Drizzle's $type(). */ import { sql } from "drizzle-orm"; import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; // ─── Plants Table ──────────────────────────────────────────────────────────── export const plants = sqliteTable( "plants", { id: text("id").primaryKey(), commonName: text("common_name").notNull(), scientificName: text("scientific_name").notNull(), family: text("family").notNull(), category: text("category").notNull(), careSummary: text("care_summary").notNull().default(""), imageUrl: text("image_url").notNull().default(""), createdAt: text("created_at") .notNull() .default(sql`(datetime('now'))`), updatedAt: text("updated_at") .notNull() .default(sql`(datetime('now'))`), }, (table) => ({ categoryIdx: index("idx_plants_category").on(table.category), commonNameIdx: index("idx_plants_common_name").on(table.commonName), }), ); // ─── Diseases Table ────────────────────────────────────────────────────────── export const diseases = sqliteTable( "diseases", { id: text("id").primaryKey(), plantId: text("plant_id") .notNull() .references(() => plants.id), name: text("name").notNull(), scientificName: text("scientific_name").notNull().default(""), causalAgentType: text("causal_agent_type", { enum: ["fungal", "bacterial", "viral", "environmental"], }).notNull(), description: text("description").notNull().default(""), symptoms: text("symptoms", { mode: "json" }).notNull().default([]).$type(), causes: text("causes", { mode: "json" }).notNull().default([]).$type(), treatment: text("treatment", { mode: "json" }).notNull().default([]).$type(), prevention: text("prevention", { mode: "json" }).notNull().default([]).$type(), lookalikeIds: text("lookalike_ids", { mode: "json" }).notNull().default([]).$type(), prevalence: text("prevalence", { enum: ["common", "uncommon", "rare", "very_rare"], }) .notNull() .default("uncommon"), prevalenceScore: integer("prevalence_score").notNull().default(0), severity: text("severity", { enum: ["low", "moderate", "high", "critical"], }).notNull(), imageUrl: text("image_url").notNull().default(""), sourceUrl: text("source_url").notNull().default(""), createdAt: text("created_at") .notNull() .default(sql`(datetime('now'))`), updatedAt: text("updated_at") .notNull() .default(sql`(datetime('now'))`), }, (table) => ({ plantIdIdx: index("idx_diseases_plant_id").on(table.plantId), causalAgentIdx: index("idx_diseases_causal_agent").on(table.causalAgentType), severityIdx: index("idx_diseases_severity").on(table.severity), prevalenceIdx: index("idx_diseases_prevalence").on(table.prevalence), }), ); // ─── Scrape Sources Table ──────────────────────────────────────────────────── export const scrapeSources = sqliteTable("scrape_sources", { id: text("id").primaryKey(), sourceType: text("source_type", { enum: ["wikipedia", "university_extension", "cabi", "other"], }).notNull(), sourceUrl: text("source_url").notNull(), lastScrapedAt: text("last_scraped_at"), entriesCount: integer("entries_count").default(0), status: text("status", { enum: ["pending", "success", "error"] }) .notNull() .default("pending"), errorMessage: text("error_message"), createdAt: text("created_at") .notNull() .default(sql`(datetime('now'))`), }); // ─── Plant Views Table ─────────────────────────────────────────────────────── export const plantViews = sqliteTable( "plant_views", { plantId: text("plant_id") .primaryKey() .references(() => plants.id), viewCount: integer("view_count").notNull().default(0), }, (table) => ({ viewCountIdx: index("idx_plant_views_count").on(table.viewCount), }), ); // ─── Flagged Content Table ───────────────────────────────────────────────── /** * Stores user-flagged content for manual review. * content_type: what kind of content is flagged * content_id: the ID of the plant or disease * field_name: specific field being flagged (e.g., "image", "symptoms", "causes", "treatment", "prevention") * flag_count: number of times this item has been flagged */ export const flaggedContent = sqliteTable( "flagged_content", { id: text("id").primaryKey(), contentType: text("content_type", { enum: [ "plant_image", "disease_image", "disease_description", "disease_symptoms", "disease_causes", "disease_treatment", "disease_prevention", ], }).notNull(), contentId: text("content_id").notNull(), fieldName: text("field_name").notNull(), notes: text("notes").default(""), flagCount: integer("flag_count").notNull().default(1), createdAt: text("created_at") .notNull() .default(sql`(datetime('now'))`), updatedAt: text("updated_at") .notNull() .default(sql`(datetime('now'))`), }, (table) => ({ contentTypeIdx: index("idx_flagged_content_type").on(table.contentType), contentIdIdx: index("idx_flagged_content_id").on(table.contentId), }), ); // ─── Type helpers ──────────────────────────────────────────────────────────── export type FlaggedContentRow = typeof flaggedContent.$inferSelect; export type FlaggedContentInsert = typeof flaggedContent.$inferInsert; // ─── Relation Inference ────────────────────────────────────────────────────── export const plantsRelations = {}; export const diseasesRelations = {}; // ─── Type helpers ──────────────────────────────────────────────────────────── export type PlantRow = typeof plants.$inferSelect; export type PlantInsert = typeof plants.$inferInsert; export type DiseaseRow = typeof diseases.$inferSelect; export type DiseaseInsert = typeof diseases.$inferInsert;