refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle
Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Database initialization and access
|
||||
*/
|
||||
|
||||
import initSqlJs from 'sql.js'
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { SCHEMA } from './schema.js'
|
||||
import { MIGRATIONS } from './migrations.js'
|
||||
import { seed, isDbEmpty } from './seed.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
// db/ is in server/, so .. = server, .. again = project root
|
||||
export const DB_PATH = join(__dirname, '..', '..', 'data', 'vps-tracker.db')
|
||||
const SEED_DIR = join(__dirname, '..', '..', 'public', 'data')
|
||||
|
||||
let dbInstance = null
|
||||
/** @type {import('sql.js').SqlJsStatic | null} */
|
||||
let sqlJsFactory = null
|
||||
|
||||
export async function initDb() {
|
||||
const dataDir = join(__dirname, '..', '..', 'data')
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
}
|
||||
|
||||
const SQL = await initSqlJs()
|
||||
sqlJsFactory = SQL
|
||||
let db
|
||||
|
||||
if (existsSync(DB_PATH)) {
|
||||
const fileBuffer = readFileSync(DB_PATH)
|
||||
db = new SQL.Database(fileBuffer)
|
||||
} else {
|
||||
db = new SQL.Database()
|
||||
}
|
||||
|
||||
db.exec(SCHEMA)
|
||||
|
||||
for (const m of MIGRATIONS) {
|
||||
try {
|
||||
m.run(db)
|
||||
} catch (err) {
|
||||
console.warn(`Migration ${m.name} failed:`, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDbEmpty(db)) {
|
||||
seed(db, SEED_DIR)
|
||||
const data = db.export()
|
||||
writeFileSync(DB_PATH, Buffer.from(data))
|
||||
}
|
||||
|
||||
dbInstance = db
|
||||
if (existsSync(DB_PATH)) {
|
||||
saveDb()
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
export function getDb() {
|
||||
if (!dbInstance) throw new Error('Database not initialized')
|
||||
return createDbWrapper(dbInstance)
|
||||
}
|
||||
|
||||
function createDbWrapper(db) {
|
||||
return {
|
||||
/** One-off run: prepare, bind, step, free. Use for statements that run once. */
|
||||
run(sql, ...params) {
|
||||
const stmt = db.prepare(sql)
|
||||
stmt.bind(params)
|
||||
stmt.step()
|
||||
const changes = db.getRowsModified()
|
||||
stmt.free()
|
||||
saveDb()
|
||||
return { changes }
|
||||
},
|
||||
prepare(sql) {
|
||||
const stmt = db.prepare(sql)
|
||||
return {
|
||||
all(...params) {
|
||||
stmt.bind(params)
|
||||
const rows = []
|
||||
while (stmt.step()) {
|
||||
rows.push(stmt.getAsObject())
|
||||
}
|
||||
stmt.free()
|
||||
return rows
|
||||
},
|
||||
get(...params) {
|
||||
stmt.bind(params)
|
||||
const row = stmt.step() ? stmt.getAsObject() : null
|
||||
stmt.free()
|
||||
return row
|
||||
},
|
||||
run(...params) {
|
||||
stmt.bind(params)
|
||||
stmt.step()
|
||||
const changes = db.getRowsModified()
|
||||
stmt.free()
|
||||
saveDb()
|
||||
return { changes }
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function saveDb() {
|
||||
if (!dbInstance) return
|
||||
const data = dbInstance.export()
|
||||
writeFileSync(DB_PATH, Buffer.from(data))
|
||||
}
|
||||
|
||||
/**
|
||||
* Заменить in-memory БД из буфера SQLite и сохранить на диск.
|
||||
* @param {Buffer|Uint8Array} buffer
|
||||
*/
|
||||
export async function reloadDatabaseFromBuffer(buffer) {
|
||||
const SQL = sqlJsFactory || (await initSqlJs())
|
||||
sqlJsFactory = SQL
|
||||
if (dbInstance) {
|
||||
dbInstance.close()
|
||||
dbInstance = null
|
||||
}
|
||||
const u8 = buffer instanceof Buffer ? new Uint8Array(buffer) : buffer
|
||||
dbInstance = new SQL.Database(u8)
|
||||
dbInstance.exec(SCHEMA)
|
||||
for (const m of MIGRATIONS) {
|
||||
try {
|
||||
m.run(dbInstance)
|
||||
} catch (err) {
|
||||
console.warn(`Migration ${m.name} after reload failed:`, err.message)
|
||||
}
|
||||
}
|
||||
saveDb()
|
||||
}
|
||||
Reference in New Issue
Block a user