feat: scaffold quiz API vertical slice
- Add GameRequestSchema and derived types to packages/shared - Add SupportedLanguageCode, SupportedPos, DifficultyLevel type exports - Add getGameTerms() model to packages/db with pos/language/difficulty/limit filters - Add prepareGameQuestions() service skeleton in apps/api - Add createGame controller with Zod safeParse validation - Wire POST /api/v1/game/start route - Add scripts/gametest/test-game.ts for manual end-to-end testing
This commit is contained in:
parent
13cc709b09
commit
9fc3ba375a
11 changed files with 99 additions and 94 deletions
|
|
@ -9,3 +9,5 @@ config({
|
|||
});
|
||||
|
||||
export const db = drizzle(process.env["DATABASE_URL"]!);
|
||||
|
||||
export { getGameTerms } from "./models/termModel.js";
|
||||
|
|
|
|||
32
packages/db/src/models/termModel.ts
Normal file
32
packages/db/src/models/termModel.ts
Normal file
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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<typeof Term>;
|
||||
|
||||
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<typeof Translation>;
|
||||
|
||||
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<typeof TermGloss>;
|
||||
|
||||
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<typeof LanguagePair>;
|
||||
|
||||
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<typeof User>;
|
||||
|
||||
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<typeof Deck>;
|
||||
|
||||
export const DeckTerm = z.object({
|
||||
deck_id: z.uuid(),
|
||||
term_id: z.uuid(),
|
||||
added: z.iso.datetime(),
|
||||
});
|
||||
|
||||
export type DeckTerm = z.infer<typeof DeckTerm>;
|
||||
|
||||
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<typeof GameRequestSchema>;
|
||||
export type GameRequestType = z.infer<typeof GameRequestSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue