diff --git a/src/repositories/mysql-card-repository.ts b/src/repositories/mysql-card-repository.ts new file mode 100644 index 0000000..8fb1498 --- /dev/null +++ b/src/repositories/mysql-card-repository.ts @@ -0,0 +1,123 @@ +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 mapRow(row: CardRow): Card { + return { + id: row.id, + question: row.question, + response: row.response, + theme: row.theme, + tags: row.tags ? JSON.parse(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) + } +} \ No newline at end of file