Files
flashcards/src/repositories/mysql-card-repository.ts
T
devcodetools f165168608 feat(deploy): image web Docker et docker-compose complet (P5)
- Dockerfile multi-étapes (node:22-alpine, pnpm, output standalone), .dockerignore
- docker-compose : service web (hôte 4000) + mysql + mongo avec volumes nommés
- Env de connexion selon DB_IMPL (MYSQL_*, MONGODB_URI) ; mongo ?authSource=admin
- Seed MySQL via 02-seed.sh (--default-character-set=utf8mb4) pour un UTF-8 propre
- fix(api): parseTags gère les colonnes JSON déjà parsées par mysql2
- TODO : phase 5 et DoD cochées
2026-09-22 09:00:07 +02:00

151 lines
4.2 KiB
TypeScript

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<Card[]> {
const [rows] = await this.pool.query<CardRow[]>(`SELECT ${FIELDS.join(', ')} FROM cards`)
return rows.map((row) => this.mapRow(row))
}
async findById(id: string): Promise<Card | null> {
const [rows] = await this.pool.query<CardRow[]>(
`SELECT ${FIELDS.join(', ')} FROM cards WHERE id = ?`,
[id],
)
return rows[0] ? this.mapRow(rows[0]) : null
}
async create(input: CardInput): Promise<Card> {
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<CardInput>): Promise<Card | null> {
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<boolean> {
const [result] = await this.pool.query<ResultSetHeader>('DELETE FROM cards WHERE id = ?', [id])
return result.affectedRows > 0
}
async findRandom(): Promise<Card | null> {
const [rows] = await this.pool.query<CardRow[]>(
`SELECT ${FIELDS.join(', ')} FROM cards ORDER BY RAND() LIMIT 1`,
)
return rows[0] ? this.mapRow(rows[0]) : null
}
async findRandomByTheme(theme: string): Promise<Card | null> {
const [rows] = await this.pool.query<CardRow[]>(
`SELECT ${FIELDS.join(', ')} FROM cards WHERE theme = ? ORDER BY RAND() LIMIT 1`,
[theme],
)
return rows[0] ? this.mapRow(rows[0]) : null
}
async findThemes(): Promise<string[]> {
const [rows] = await this.pool.query<RowDataPacket[]>(
'SELECT DISTINCT theme FROM cards ORDER BY theme',
)
return rows.map((row) => row.theme as string)
}
}