diff --git a/src/repositories/mongo-card-repository.ts b/src/repositories/mongo-card-repository.ts new file mode 100644 index 0000000..4f7f29c --- /dev/null +++ b/src/repositories/mongo-card-repository.ts @@ -0,0 +1,126 @@ +import { MongoClient, type Collection } from 'mongodb' +import type { Card, CardInput } from '../domain/card' +import type { CardRepository } from '../domain/card-repository' + +interface CardDocument { + c_id: string + question: string + response: string + theme: string + tags: string[] + createdAt: Date + updatedAt: Date +} + +const DOC_FIELDS = { + _id: 0, + c_id: 1, + question: 1, + response: 1, + theme: 1, + tags: 1, + createdAt: 1, + updatedAt: 1, +} + +export class MongoCardRepository implements CardRepository { + private readonly collection: Collection + + constructor(collection?: Collection) { + if (collection) { + this.collection = collection + } else { + const client = new MongoClient( + process.env.MONGODB_URI ?? 'mongodb://flashcards:flashcards@localhost:127017/flashcards', + ) + this.collection = client + .db(process.env.MONGODB_DATABASE ?? 'flashcards') + .collection('cards') + } + } + + private mapDoc(doc: CardDocument): Card { + return { + id: doc.c_id, + question: doc.question, + response: doc.response, + theme: doc.theme, + tags: doc.tags, + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + } + } + + async findAll(): Promise { + const docs = await this.collection.find({}, { projection: DOC_FIELDS }).toArray() + return docs.map((doc) => this.mapDoc(doc)) + } + + async findById(id: string): Promise { + const doc = await this.collection.findOne({ c_id: id }, { projection: DOC_FIELDS }) + return doc ? this.mapDoc(doc) : null + } + + async create(input: CardInput): Promise { + const doc: CardDocument = { + c_id: crypto.randomUUID(), + question: input.question, + response: input.response, + theme: input.theme, + tags: input.tags ?? [], + createdAt: new Date(), + updatedAt: new Date(), + } + await this.collection.insertOne(doc) + return this.mapDoc(doc) + } + + async update(id: string, input: Partial): Promise { + const set: Partial = { updatedAt: new Date() } + if (input.question !== undefined) { + set.question = input.question + } + if (input.response !== undefined) { + set.response = input.response + } + if (input.theme !== undefined) { + set.theme = input.theme + } + if (input.tags !== undefined) { + set.tags = input.tags + } + const result = await this.collection.updateOne({ c_id: id }, { $set: set }) + if (result.matchedCount === 0) { + return null + } + return this.findById(id) + } + + async delete(id: string): Promise { + const result = await this.collection.deleteOne({ c_id: id }) + return result.deletedCount > 0 + } + + async findRandom(): Promise { + const docs = await this.collection + .aggregate([{ $sample: { size: 1 } }, { $project: DOC_FIELDS }]) + .toArray() + return docs[0] ? this.mapDoc(docs[0]) : null + } + + async findRandomByTheme(theme: string): Promise { + const docs = await this.collection + .aggregate([ + { $match: { theme } }, + { $sample: { size: 1 } }, + { $project: DOC_FIELDS }, + ]) + .toArray() + return docs[0] ? this.mapDoc(docs[0]) : null + } + + async findThemes(): Promise { + const themes: string[] = await this.collection.distinct('theme') + return themes.sort() + } +} \ No newline at end of file