Skip to content

Commit 7390d43

Browse files
committed
Converting to mongo
1 parent d9fa8ee commit 7390d43

File tree

14 files changed

+989
-261
lines changed

14 files changed

+989
-261
lines changed

app/actions/cards.ts

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
'use server';
2+
3+
import { ObjectId } from 'mongodb';
4+
import { revalidatePath } from 'next/cache';
5+
import type { Card, CardDocument, ReviewEventDocument } from '@/types';
6+
import { ReviewResult } from '@/types'; // Import enum value
7+
// import jwt from 'jsonwebtoken'; // Removed
8+
import { connectToDatabase } from '@/lib/db';
9+
import { verifyAuthToken } from '@/lib/auth'; // Import shared auth function
10+
import { mapMongoId } from '@/lib/utils'; // Import shared helper
11+
12+
// --- Remove Auth Helper ---
13+
// function verifyAuthToken(token: string | undefined): JwtPayload | null { ... } // Removed
14+
15+
// --- Remove Generic Helper ---
16+
// function mapMongoId<T extends { _id?: ObjectId }>(...) { ... } // Removed
17+
18+
// --- Keep Specific Helper ---
19+
function mapCardDocument(doc: CardDocument | null | undefined): Card | null {
20+
const mapped = mapMongoId(doc); // Use imported helper
21+
if (mapped && mapped.deck_id instanceof ObjectId) {
22+
return {
23+
...mapped,
24+
deck_id: mapped.deck_id.toString(),
25+
} as Card;
26+
}
27+
// Handle cases where mapping fails or deck_id isn't ObjectId (shouldn't happen with proper types)
28+
return null;
29+
}
30+
31+
// --- Card Actions ---
32+
33+
interface CreateCardResult {
34+
success: boolean;
35+
card?: Card; // Use shared Card type
36+
message?: string;
37+
}
38+
39+
interface CreateCardInput {
40+
deckId: string;
41+
frontText: string;
42+
backText: string;
43+
token: string | undefined;
44+
}
45+
46+
export async function createCardAction(input: CreateCardInput): Promise<CreateCardResult> {
47+
const { deckId, frontText, backText, token } = input;
48+
49+
const user = verifyAuthToken(token);
50+
if (!user) return { success: false, message: 'Unauthorized' };
51+
52+
if (!deckId || !ObjectId.isValid(deckId)) {
53+
return { success: false, message: 'Valid Deck ID is required' };
54+
}
55+
if (!frontText || !frontText.trim()) {
56+
return { success: false, message: 'Front text cannot be empty' };
57+
}
58+
if (!backText || !backText.trim()) {
59+
return { success: false, message: 'Back text cannot be empty' };
60+
}
61+
62+
// No client variable needed
63+
try {
64+
const { db } = await connectToDatabase(); // Use imported function
65+
const cardsCollection = db.collection<CardDocument>('cards');
66+
67+
const newCardData: Omit<CardDocument, '_id'> = {
68+
deck_id: new ObjectId(deckId),
69+
front_text: frontText.trim(),
70+
back_text: backText.trim(),
71+
createdAt: new Date(),
72+
updatedAt: new Date(),
73+
};
74+
75+
const result = await cardsCollection.insertOne(newCardData as CardDocument);
76+
77+
if (!result.insertedId) {
78+
throw new Error('Card creation failed in DB.');
79+
}
80+
81+
const createdCardDoc = await cardsCollection.findOne({ _id: result.insertedId });
82+
// Use specific card mapper
83+
const mappedCreatedCard = mapCardDocument(createdCardDoc);
84+
85+
if (!mappedCreatedCard) {
86+
throw new Error('Failed to map created card.');
87+
}
88+
89+
// No client.close() needed
90+
revalidatePath(`/deck/${deckId}/edit`);
91+
return { success: true, card: mappedCreatedCard }; // Already type Card
92+
93+
} catch (error) {
94+
console.error('[Create Card Action Error]', error);
95+
// No client?.close() needed
96+
const message = error instanceof Error ? error.message : 'Failed to create card';
97+
return { success: false, message };
98+
}
99+
}
100+
101+
// --- Fetch Cards for a Deck ---
102+
interface FetchCardsResult {
103+
success: boolean;
104+
cards?: Card[]; // Use shared Card type
105+
message?: string;
106+
}
107+
108+
export async function fetchDeckCardsAction(deckId: string): Promise<FetchCardsResult> {
109+
if (!deckId || !ObjectId.isValid(deckId)) {
110+
return { success: false, message: 'Valid Deck ID is required' };
111+
}
112+
113+
try {
114+
const { db } = await connectToDatabase();
115+
const cardsCollection = db.collection<CardDocument>('cards');
116+
117+
const cardDocs = await cardsCollection.find({ deck_id: new ObjectId(deckId) }).sort({ createdAt: 1 }).toArray();
118+
const mappedCards = cardDocs.map(mapCardDocument).filter((c): c is Card => c !== null);
119+
120+
return { success: true, cards: mappedCards };
121+
122+
} catch (error) {
123+
console.error('[Fetch Deck Cards Action Error]', error);
124+
const message = error instanceof Error ? error.message : 'Failed to fetch cards for deck';
125+
return { success: false, message };
126+
}
127+
}
128+
129+
// --- Update Card ---
130+
interface UpdateCardResult {
131+
success: boolean;
132+
card?: Card;
133+
message?: string;
134+
}
135+
136+
interface UpdateCardInput {
137+
cardId: string;
138+
deckId: string; // Needed for revalidation
139+
frontText?: string;
140+
backText?: string;
141+
token: string | undefined;
142+
}
143+
144+
export async function updateCardAction(input: UpdateCardInput): Promise<UpdateCardResult> {
145+
const { cardId, deckId, frontText, backText, token } = input;
146+
147+
const user = verifyAuthToken(token);
148+
if (!user) return { success: false, message: 'Unauthorized' };
149+
150+
if (!cardId || !ObjectId.isValid(cardId) || !deckId || !ObjectId.isValid(deckId)) {
151+
return { success: false, message: 'Valid Card ID and Deck ID are required' };
152+
}
153+
if ((!frontText || !frontText.trim()) && (!backText || !backText.trim())) {
154+
return { success: false, message: 'At least one field (front or back text) must be provided for update' };
155+
}
156+
157+
const updates: Partial<Pick<CardDocument, 'front_text' | 'back_text' | 'updatedAt'>> = {
158+
updatedAt: new Date(),
159+
};
160+
if (frontText && frontText.trim()) updates.front_text = frontText.trim();
161+
if (backText && backText.trim()) updates.back_text = backText.trim();
162+
163+
try {
164+
const { db } = await connectToDatabase();
165+
const cardsCollection = db.collection<CardDocument>('cards');
166+
167+
const result = await cardsCollection.findOneAndUpdate(
168+
{ _id: new ObjectId(cardId), deck_id: new ObjectId(deckId) }, // Ensure card belongs to the deck
169+
{ $set: updates },
170+
{ returnDocument: 'after' }
171+
);
172+
173+
const mappedUpdatedCard = mapCardDocument(result);
174+
175+
if (!mappedUpdatedCard) {
176+
return { success: false, message: 'Card not found for update or update failed' };
177+
}
178+
179+
revalidatePath(`/deck/${deckId}/edit`);
180+
return { success: true, card: mappedUpdatedCard };
181+
182+
} catch (error) {
183+
console.error('[Update Card Action Error]', error);
184+
const message = error instanceof Error ? error.message : 'Failed to update card';
185+
return { success: false, message };
186+
}
187+
}
188+
189+
// --- Delete Card ---
190+
interface DeleteCardResult {
191+
success: boolean;
192+
message?: string;
193+
}
194+
195+
interface DeleteCardInput {
196+
cardId: string;
197+
deckId: string; // Needed for revalidation and verification
198+
token: string | undefined;
199+
}
200+
201+
export async function deleteCardAction(input: DeleteCardInput): Promise<DeleteCardResult> {
202+
const { cardId, deckId, token } = input;
203+
204+
const user = verifyAuthToken(token);
205+
if (!user) return { success: false, message: 'Unauthorized' };
206+
207+
if (!cardId || !ObjectId.isValid(cardId) || !deckId || !ObjectId.isValid(deckId)) {
208+
return { success: false, message: 'Valid Card ID and Deck ID are required' };
209+
}
210+
211+
try {
212+
const { db } = await connectToDatabase();
213+
const cardsCollection = db.collection<CardDocument>('cards');
214+
215+
// TODO: Delete associated Review Events if necessary
216+
const result = await cardsCollection.deleteOne({ _id: new ObjectId(cardId), deck_id: new ObjectId(deckId) });
217+
218+
if (result.deletedCount === 0) {
219+
return { success: false, message: 'Card not found or does not belong to the specified deck' };
220+
}
221+
222+
revalidatePath(`/deck/${deckId}/edit`);
223+
return { success: true };
224+
225+
} catch (error) {
226+
console.error('[Delete Card Action Error]', error);
227+
const message = error instanceof Error ? error.message : 'Failed to delete card';
228+
return { success: false, message };
229+
}
230+
}
231+
232+
// --- Create Review Event ---
233+
interface CreateReviewEventResult {
234+
success: boolean;
235+
reviewEventId?: string; // Return the ID of the created event
236+
message?: string;
237+
}
238+
239+
interface CreateReviewEventInput {
240+
cardId: string;
241+
deckId: string; // Might be useful for context/validation
242+
result: ReviewResult;
243+
// No token needed if we decide reviews are public/unauthenticated
244+
// Add token if reviews should be tied to a logged-in user
245+
}
246+
247+
export async function createReviewEventAction(input: CreateReviewEventInput): Promise<CreateReviewEventResult> {
248+
const { cardId, deckId, result } = input;
249+
250+
// Validate input
251+
if (!cardId || !ObjectId.isValid(cardId)) {
252+
return { success: false, message: 'Valid Card ID is required' };
253+
}
254+
if (!deckId || !ObjectId.isValid(deckId)) {
255+
// Optional: Could fetch card to verify deckId association
256+
return { success: false, message: 'Valid Deck ID is required' };
257+
}
258+
// Check if result is a valid enum value
259+
if (!Object.values(ReviewResult).includes(result)) {
260+
return { success: false, message: 'Invalid review result value' };
261+
}
262+
263+
// TODO: Add auth check here if reviews require login
264+
// const user = verifyAuthToken(token);
265+
// if (!user) return { success: false, message: 'Unauthorized' };
266+
267+
try {
268+
const { db } = await connectToDatabase();
269+
const reviewsCollection = db.collection<ReviewEventDocument>('review_events'); // Use a separate collection
270+
271+
const newReviewEventData: Omit<ReviewEventDocument, '_id'> = {
272+
card_id: new ObjectId(cardId),
273+
// deck_id: new ObjectId(deckId), // Optionally store deck_id too
274+
result: result,
275+
timestamp: new Date(), // Use server timestamp
276+
};
277+
278+
const dbResult = await reviewsCollection.insertOne(newReviewEventData as ReviewEventDocument);
279+
280+
if (!dbResult.insertedId) {
281+
throw new Error('Review event creation failed in DB.');
282+
}
283+
284+
// No revalidation needed typically for review events unless displaying history
285+
// revalidatePath(...)
286+
287+
return { success: true, reviewEventId: dbResult.insertedId.toString() };
288+
289+
} catch (error) {
290+
console.error('[Create Review Event Action Error]', error);
291+
const message = error instanceof Error ? error.message : 'Failed to record review event';
292+
return { success: false, message };
293+
}
294+
}
295+
296+
// Remove duplicated example
297+
/*
298+
export async function fetchDecksAction(): Promise<{ success: boolean; decks?: Deck[]; message?: string }> {
299+
// ... implementation ...
300+
}
301+
*/

0 commit comments

Comments
 (0)