diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 5a8122a..367b101 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -4,7 +4,7 @@ import { apiRouter } from "./routes/apiRouter.js"; export function createApp() { const app: Express = express(); - + app.use(express.json()); app.use("/api/v1", apiRouter); return app; diff --git a/apps/api/src/controllers/gameController.ts b/apps/api/src/controllers/gameController.ts index ffffbee..eec4558 100644 --- a/apps/api/src/controllers/gameController.ts +++ b/apps/api/src/controllers/gameController.ts @@ -1,8 +1,17 @@ import type { Request, Response } from "express"; -import { prepareQuestions } from "../services/gameService.js"; +import { GameRequestSchema } from "@glossa/shared"; +import { prepareGameQuestions } from "../services/gameService.js"; -export const getGame = async (req: Request, res: Response) => { - const query = gameRequestSchema.parse(req.query); - const questions = await prepareQuestions(query); - res.json({ success: true, data: questions }); +export const createGame = async (req: Request, res: Response) => { + const gameSettings = GameRequestSchema.safeParse(req.body); + + // TODO: remove when global error handler is implemented + if (!gameSettings.success) { + res.status(400).json({ success: false }); + return; + } + + const gameQuestions = await prepareGameQuestions(gameSettings.data); + + res.json({ success: true, data: gameQuestions }); }; diff --git a/apps/api/src/routes/gameRouter.ts b/apps/api/src/routes/gameRouter.ts index 7196021..d1e4b33 100644 --- a/apps/api/src/routes/gameRouter.ts +++ b/apps/api/src/routes/gameRouter.ts @@ -4,4 +4,4 @@ import { createGame } from "../controllers/gameController.js"; export const gameRouter: Router = express.Router(); -gameRouter.get("/", createGame); +gameRouter.post("/start", createGame); diff --git a/apps/api/src/services/gameService.ts b/apps/api/src/services/gameService.ts index f4c79d1..9478d3f 100644 --- a/apps/api/src/services/gameService.ts +++ b/apps/api/src/services/gameService.ts @@ -1,3 +1,16 @@ -export const prepareQuestions = async (params: object) => { - console.log(params); +import type { GameRequestType } from "@glossa/shared"; +import { getGameTerms } from "@glossa/db"; + +export const prepareGameQuestions = async (gameSettings: GameRequestType) => { + const { source_language, target_language, pos, difficulty, rounds } = + gameSettings; + + const terms = await getGameTerms( + source_language, + target_language, + pos, + difficulty, + Number(rounds), + ); + return terms; }; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index b470f84..399a8af 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "references": [ { "path": "../../packages/shared" }, - { "path": "../../packages/db" } + { "path": "../../packages/db" }, ], "compilerOptions": { "module": "NodeNext", @@ -10,7 +10,7 @@ "outDir": "./dist", "resolveJsonModule": true, "rootDir": ".", - "types": ["vitest/globals"] + "types": ["vitest/globals"], }, - "include": ["src", "vitest.config.ts"] + "include": ["src", "vitest.config.ts", "../../packages/db/src/models"], } diff --git a/documentation/roadmap.md b/documentation/roadmap.md index 871cab2..ed09f21 100644 --- a/documentation/roadmap.md +++ b/documentation/roadmap.md @@ -159,7 +159,7 @@ this would be the flow to start a single player game: main menu => singleplayer, multiplayer, settings singleplayer => language selection "i speak english" => "i want to learn italian" (both languages are dropdowns to select the fitting language) -language selection => category selection => pure grammar, media (as disussed, practicing on song lyrics or breaking bad subtitles) +language selection => category selection => pure grammar, media (practicing on song lyrics or breaking bad subtitles) pure grammar => pos selection => nouns or verbs (in mvp) nouns has 3 subcategories => singular (1-on-1 translation dog => cane), plural (plural practices cane => cani for example), gender/articles (il cane or la cane for example) verbs has 2 subcategories => infinitv (1-on-1 translation to talk => parlare) or conjugations (user gets shown the infinitiv and a table with all personal pronouns and has to fill in the gaps with the according conjugations) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index b001f8b..06232cb 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -9,3 +9,5 @@ config({ }); export const db = drizzle(process.env["DATABASE_URL"]!); + +export { getGameTerms } from "./models/termModel.js"; diff --git a/packages/db/src/models/termModel.ts b/packages/db/src/models/termModel.ts new file mode 100644 index 0000000..b1efea3 --- /dev/null +++ b/packages/db/src/models/termModel.ts @@ -0,0 +1,32 @@ +import { db } from "@glossa/db"; +import { eq, and } from "drizzle-orm"; +import { terms, translations } from "@glossa/db/schema"; + +import type { + SupportedLanguageCode, + SupportedPos, + DifficultyLevel, +} from "@glossa/shared"; + +export const getGameTerms = async ( + sourceLanguage: SupportedLanguageCode, + targetLanguage: SupportedLanguageCode, + pos: SupportedPos, + difficulty: DifficultyLevel, + count: number, +) => { + const rows = await db + .select() + .from(terms) + .innerJoin(translations, eq(translations.term_id, terms.id)) + .where( + and( + eq(terms.pos, pos), + eq(translations.language_code, targetLanguage), + eq(translations.difficulty, difficulty), + ), + ) + .limit(count); + + return rows; +}; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 4d87b1d..b0ae2f3 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -1,11 +1,15 @@ export const SUPPORTED_LANGUAGE_CODES = ["en", "it"] as const; +export type SupportedLanguageCode = (typeof SUPPORTED_LANGUAGE_CODES)[number]; export const SUPPORTED_POS = ["noun", "verb"] as const; +export type SupportedPos = (typeof SUPPORTED_POS)[number]; export const GAME_ROUNDS = ["3", "10"] as const; +export type GameRounds = (typeof GAME_ROUNDS)[number]; export const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"] as const; export const SUPPORTED_DECK_TYPES = ["grammar", "media"] as const; export const DIFFICULTY_LEVELS = ["easy", "intermediate", "hard"] as const; +export type DifficultyLevel = (typeof DIFFICULTY_LEVELS)[number]; diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts index aeb8a56..a0647a1 100644 --- a/packages/shared/src/schema.ts +++ b/packages/shared/src/schema.ts @@ -4,88 +4,15 @@ import { SUPPORTED_LANGUAGE_CODES, SUPPORTED_POS, GAME_ROUNDS, + DIFFICULTY_LEVELS, } from "./constants.js"; -export const Term = z.object({ - id: z.uuid(), - synset_id: z.string(), - pos: z.string(), - created_at: z.iso.datetime(), +export const GameRequestSchema = z.object({ + source_language: z.enum(SUPPORTED_LANGUAGE_CODES), + target_language: z.enum(SUPPORTED_LANGUAGE_CODES), + pos: z.enum(SUPPORTED_POS), + difficulty: z.enum(DIFFICULTY_LEVELS), + rounds: z.enum(GAME_ROUNDS), }); -export type Term = z.infer; - -export const Translation = z.object({ - id: z.uuid(), - term_id: z.string(), - language_code: z.string(), - text: z.string(), - created_at: z.iso.datetime(), -}); - -export type Translation = z.infer; - -export const TermGloss = z.object({ - id: z.uuid(), - term_id: z.uuid(), - language_code: z.string(), - text: z.string(), - created_at: z.iso.datetime(), -}); - -export type TermGloss = z.infer; - -export const LanguagePair = z.object({ - id: z.uuid(), - source_language: z.string(), - target_language: z.string(), - label: z.string(), - active: z.boolean(), - created_at: z.iso.datetime(), -}); - -export type LanguagePair = z.infer; - -export const User = z.object({ - id: z.uuid(), - openauth_sub: z.string(), - email: z.email(), - display_name: z.string(), - created_at: z.iso.datetime(), - last_login_at: z.iso.datetime(), -}); - -export type User = z.infer; - -export const Deck = z.object({ - id: z.uuid(), - name: z.string(), - description: z.string(), - source_language: z.string(), - validated_languages: z.array(z.string()), - is_public: z.boolean(), - created_at: z.iso.datetime(), -}); - -export type Deck = z.infer; - -export const DeckTerm = z.object({ - deck_id: z.uuid(), - term_id: z.uuid(), - added: z.iso.datetime(), -}); - -export type DeckTerm = z.infer; - -export const GameRequestSchema = z - .object({ - source_language: z.enum(SUPPORTED_LANGUAGE_CODES), - target_language: z.enum(SUPPORTED_LANGUAGE_CODES), - pos: z.enum(SUPPORTED_POS), - rounds: z.enum(GAME_ROUNDS).transform(Number), - }) - .refine((game) => game.target_language !== game.source_language, { - error: "source and target language must be different", - }); - -export type GameRequestSchema = z.infer; +export type GameRequestType = z.infer; diff --git a/scripts/gametest/test-game.ts b/scripts/gametest/test-game.ts new file mode 100644 index 0000000..5982633 --- /dev/null +++ b/scripts/gametest/test-game.ts @@ -0,0 +1,18 @@ +async function main() { + const response = await fetch("http://localhost:3000/api/v1/game/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source_language: "en", + target_language: "it", + pos: "noun", + difficulty: "easy", + rounds: "3", + }), + }); + + const data = await response.json(); + console.log(data); +} + +main();