feat: 添加SQLite本地数据库支持

- 移除Prisma中不支持SQLite的类型(Json、枚举)
- 使用String类型替代枚举值
- 更新Prisma schema适配SQLite
- 添加数据库初始化脚本scripts/init-db.js
- 更新数据库路径配置
- 添加sql.js依赖
- 删除旧的prisma.ts使用新的db.ts

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-01-29 16:33:55 +08:00
parent e35bd77e06
commit fbff8cc230
4 changed files with 127 additions and 15 deletions

View File

@ -1,6 +1,7 @@
{ {
"name": "qia-server", "name": "qia-server",
"version": "0.1.0", "version": "0.1.0",
"type": "module",
"description": "Backend API for 掐日子(qia) app", "description": "Backend API for 掐日子(qia) app",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@ -23,6 +24,7 @@
"express-validator": "^7.1.0", "express-validator": "^7.1.0",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"sql.js": "^1.13.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {

View File

@ -31,12 +31,12 @@ model User {
model Event { model Event {
id String @id @default(uuid()) id String @id @default(uuid())
user_id String user_id String
type EventType // 'anniversary' | 'reminder' type String // 'anniversary' | 'reminder' (String for SQLite)
title String title String
content String? // Only for reminders content String? // Only for reminders
date DateTime // For anniversaries: the date; For reminders: the reminder date date DateTime // For anniversaries: the date; For reminders: the reminder date
is_lunar Boolean @default(false) is_lunar Boolean @default(false)
repeat_type RepeatType @default(none) repeat_type String @default("none") // 'yearly' | 'monthly' | 'none' (String for SQLite)
is_holiday Boolean @default(false) // Only for anniversaries is_holiday Boolean @default(false) // Only for anniversaries
is_completed Boolean @default(false) // Only for reminders is_completed Boolean @default(false) // Only for reminders
created_at DateTime @default(now()) created_at DateTime @default(now())
@ -55,7 +55,7 @@ model Event {
model Note { model Note {
id String @id @default(uuid()) id String @id @default(uuid())
user_id String user_id String
content String @db.Text // HTML content from rich text editor content String // HTML content from rich text editor
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
@ -73,7 +73,7 @@ model AICoachConversation {
user_id String user_id String
message String message String
response String response String
parsed_data Json? // AI parsed event data parsed_data String? // AI parsed event data (JSON string for SQLite)
created_at DateTime @default(now()) created_at DateTime @default(now())
// Relations // Relations
@ -83,14 +83,6 @@ model AICoachConversation {
@@map("ai_coach_conversations") @@map("ai_coach_conversations")
} }
// Enums // Note: Using String types instead of enums for SQLite compatibility
enum EventType { // Event type values: 'anniversary' | 'reminder'
anniversary // Repeat type values: 'yearly' | 'monthly' | 'none'
reminder
}
enum RepeatType {
yearly
monthly
none
}

98
scripts/init-db.js Normal file
View File

@ -0,0 +1,98 @@
import { createClient } from '@libsql/client';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Navigate from scripts/ to server/ root, then to prisma/
const dbPath = path.join(__dirname, '..', 'prisma', 'dev.db');
const prismaDir = path.dirname(dbPath);
// Ensure prisma directory exists
if (!fs.existsSync(prismaDir)) {
fs.mkdirSync(prismaDir, { recursive: true });
console.log('Created prisma directory');
}
console.log('Database path:', dbPath);
// Ensure file exists
if (!fs.existsSync(dbPath)) {
fs.writeFileSync(dbPath, '');
console.log('Created empty database file');
}
const db = createClient({
url: `file:${dbPath}`,
});
console.log('LibSQL client created');
// Create tables
await db.execute(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
nickname TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('Created users table');
await db.execute(`
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT,
date DATETIME NOT NULL,
is_lunar INTEGER DEFAULT 0,
repeat_type TEXT DEFAULT 'none',
is_holiday INTEGER DEFAULT 0,
is_completed INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
console.log('Created events table');
await db.execute(`CREATE INDEX IF NOT EXISTS idx_events_user_id ON events(user_id)`);
await db.execute(`CREATE INDEX IF NOT EXISTS idx_events_type ON events(type)`);
await db.execute(`CREATE INDEX IF NOT EXISTS idx_events_date ON events(date)`);
await db.execute(`
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
user_id TEXT UNIQUE NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
console.log('Created notes table');
await db.execute(`
CREATE TABLE IF NOT EXISTS ai_coach_conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
message TEXT NOT NULL,
response TEXT NOT NULL,
parsed_data TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
console.log('Created ai_coach_conversations table');
await db.execute(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_user_id ON ai_coach_conversations(user_id)`);
// Verify
const tables = await db.execute("SELECT name FROM sqlite_master WHERE type='table'");
console.log('Tables:', tables.rows.map(r => r.name).join(', '));
console.log('\nDatabase initialized successfully!');

20
src/lib/db.ts Normal file
View File

@ -0,0 +1,20 @@
import { createClient } from '@libsql/client';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// Load .env file
dotenv.config();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = path.resolve(__dirname, '../../prisma/dev.db');
console.log('Database path:', dbPath);
// Create LibSQL client
const db = createClient({
url: `file:${dbPath}`,
});
export default db;
export { dbPath };