re-init
This commit is contained in:
69
src/lib/db/index.ts
Normal file
69
src/lib/db/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Drizzle ORM Database Client for Turso/libSQL.
|
||||
*
|
||||
* Provides the configured drizzle instance and convenience helpers.
|
||||
* Reads DATABASE_URL and DATABASE_TOKEN from environment.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { drizzle, type LibSQLDatabase } from "drizzle-orm/libsql";
|
||||
import { createClient } from "@libsql/client";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export type {
|
||||
PlantRow,
|
||||
PlantInsert,
|
||||
DiseaseRow,
|
||||
DiseaseInsert,
|
||||
FlaggedContentRow,
|
||||
FlaggedContentInsert,
|
||||
} from "./schema";
|
||||
|
||||
export { schema };
|
||||
|
||||
let _db: LibSQLDatabase<typeof schema> | null = null;
|
||||
let _client: ReturnType<typeof createClient> | null = null;
|
||||
|
||||
/** Get or create the Drizzle database instance (singleton). */
|
||||
export function getDb(): LibSQLDatabase<typeof schema> {
|
||||
if (_db) return _db;
|
||||
|
||||
const url = process.env.DATABASE_URL;
|
||||
const token = process.env.DATABASE_TOKEN;
|
||||
|
||||
if (!url) {
|
||||
throw new Error(
|
||||
"DATABASE_URL is not set. Check your .env.development or .env.production file.",
|
||||
);
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"DATABASE_TOKEN is not set. Check your .env.development or .env.production file.",
|
||||
);
|
||||
}
|
||||
|
||||
_client = createClient({ url, authToken: token });
|
||||
_db = drizzle(_client, { schema });
|
||||
return _db;
|
||||
}
|
||||
|
||||
/** Check database connectivity. */
|
||||
export async function checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
const db = getDb();
|
||||
const result = await db.run(sql`SELECT 1 AS ok`);
|
||||
return result.rowsAffected >= 0;
|
||||
} catch (err) {
|
||||
console.error("[DB] Connection failed:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Close the client connection. */
|
||||
export function closeDb() {
|
||||
if (_client) {
|
||||
_client.close();
|
||||
_client = null;
|
||||
_db = null;
|
||||
}
|
||||
}
|
||||
173
src/lib/db/schema.ts
Normal file
173
src/lib/db/schema.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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<string[]>(),
|
||||
causes: text("causes", { mode: "json" }).notNull().default([]).$type<string[]>(),
|
||||
treatment: text("treatment", { mode: "json" }).notNull().default([]).$type<string[]>(),
|
||||
prevention: text("prevention", { mode: "json" }).notNull().default([]).$type<string[]>(),
|
||||
lookalikeIds: text("lookalike_ids", { mode: "json" }).notNull().default([]).$type<string[]>(),
|
||||
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;
|
||||
Reference in New Issue
Block a user