import { createPool, type Pool, type ResultSetHeader, type RowDataPacket } from 'mysql2/promise' import type { Card, CardInput } from '../domain/card' import type { CardRepository } from '../domain/card-repository' interface CardRow extends RowDataPacket { id: string question: string response: string theme: string tags: string | null createdAt: Date updatedAt: Date } const FIELDS = ['id', 'question', 'response', 'theme', 'tags', 'createdAt', 'updatedAt'] as const export class MysqlCardRepository implements CardRepository { private readonly pool: Pool constructor(pool?: Pool) { this.pool = pool ?? createPool({ host: process.env.MYSQL_HOST ?? 'localhost', port: Number(process.env.MYSQL_PORT ?? 13306), user: process.env.MYSQL_USER ?? 'flashcards', password: process.env.MYSQL_PASSWORD ?? 'flashcards', database: process.env.MYSQL_DATABASE ?? 'flashcards', }) } private parseTags(value: string | unknown): string[] { if (Array.isArray(value)) { return value as string[] } if (typeof value === 'string') { try { const parsed = JSON.parse(value) return Array.isArray(parsed) ? parsed : [] } catch { return [] } } return [] } private mapRow(row: CardRow): Card { return { id: row.id, question: row.question, response: row.response, theme: row.theme, tags: this.parseTags(row.tags), createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), } } async findAll(): Promise { const [rows] = await this.pool.query(`SELECT ${FIELDS.join(', ')} FROM cards`) return rows.map((row) => this.mapRow(row)) } async findById(id: string): Promise { const [rows] = await this.pool.query( `SELECT ${FIELDS.join(', ')} FROM cards WHERE id = ?`, [id], ) return rows[0] ? this.mapRow(rows[0]) : null } async create(input: CardInput): Promise { const id = crypto.randomUUID() const now = new Date() await this.pool.query(`INSERT INTO cards (${FIELDS.join(', ')}) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ id, input.question, input.response, input.theme, JSON.stringify(input.tags ?? []), now, now, ]) return { id, question: input.question, response: input.response, theme: input.theme, tags: input.tags ?? [], createdAt: now, updatedAt: now, } } async update(id: string, input: Partial): Promise { const sets: string[] = [] const values: unknown[] = [] if (input.question !== undefined) { sets.push('question = ?') values.push(input.question) } if (input.response !== undefined) { sets.push('response = ?') values.push(input.response) } if (input.theme !== undefined) { sets.push('theme = ?') values.push(input.theme) } if (input.tags !== undefined) { sets.push('tags = ?') values.push(JSON.stringify(input.tags)) } if (sets.length === 0) { return this.findById(id) } sets.push('updatedAt = ?') values.push(new Date()) values.push(id) await this.pool.query(`UPDATE cards SET ${sets.join(', ')} WHERE id = ?`, values) return this.findById(id) } async delete(id: string): Promise { const [result] = await this.pool.query('DELETE FROM cards WHERE id = ?', [id]) return result.affectedRows > 0 } async findRandom(): Promise { const [rows] = await this.pool.query( `SELECT ${FIELDS.join(', ')} FROM cards ORDER BY RAND() LIMIT 1`, ) return rows[0] ? this.mapRow(rows[0]) : null } async findRandomByTheme(theme: string): Promise { const [rows] = await this.pool.query( `SELECT ${FIELDS.join(', ')} FROM cards WHERE theme = ? ORDER BY RAND() LIMIT 1`, [theme], ) return rows[0] ? this.mapRow(rows[0]) : null } async findThemes(): Promise { const [rows] = await this.pool.query( 'SELECT DISTINCT theme FROM cards ORDER BY theme', ) return rows.map((row) => row.theme as string) } }