feat: [backend] Create BFF for adventure
This commit is contained in:
parent
48bbcac9a6
commit
85699fb9e5
6 changed files with 317 additions and 4 deletions
138
api/app/routers/bff/adventure.py
Normal file
138
api/app/routers/bff/adventure.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ...auth import verify_token
|
||||
from ...config import settings
|
||||
from ...outbound.postgres.database import get_db
|
||||
from ...outbound.postgres.repositories.adventure_repository import (
|
||||
PostgresAdventureEntryAudioRepository,
|
||||
PostgresAdventureEntryChoiceRepository,
|
||||
PostgresAdventureEntryRepository,
|
||||
PostgresAdventureEntryTranslationRepository,
|
||||
PostgresAdventureRepository,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/adventure", tags=["bff", "adventures"])
|
||||
|
||||
|
||||
class AdventureChoiceItem(BaseModel):
|
||||
id: str
|
||||
index: int
|
||||
label: str
|
||||
text: str
|
||||
|
||||
|
||||
class AdventureEntryItem(BaseModel):
|
||||
id: str
|
||||
adventure_id: str
|
||||
generated_from_choice_id: str | None
|
||||
status: str
|
||||
entry_index: int
|
||||
story_text: str | None
|
||||
translation: str | None
|
||||
audio_url: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class AdventureDetailResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
status: str
|
||||
language: str
|
||||
source_language: str
|
||||
competencies: list[str]
|
||||
max_entry_count: int
|
||||
title: str
|
||||
description: str | None
|
||||
genres: list[str]
|
||||
setting: list[str]
|
||||
vibes: list[str]
|
||||
protagonist: list[str]
|
||||
created_at: str
|
||||
entries: list[AdventureEntryItem]
|
||||
current_entry_choices: list[AdventureChoiceItem]
|
||||
|
||||
|
||||
def _audio_url(key: str | None) -> str | None:
|
||||
if key is None:
|
||||
return None
|
||||
return f"{settings.api_base_url}/media/{key}"
|
||||
|
||||
|
||||
@router.get("/{adventure_id}", response_model=AdventureDetailResponse, status_code=200)
|
||||
async def get_adventure(
|
||||
adventure_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
token_data: dict = Depends(verify_token),
|
||||
) -> AdventureDetailResponse:
|
||||
user_id = uuid.UUID(token_data["sub"])
|
||||
|
||||
try:
|
||||
adv_id = uuid.UUID(adventure_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid adventure_id")
|
||||
|
||||
adventure = await PostgresAdventureRepository(db).get_by_id(adv_id)
|
||||
if adventure is None or adventure.user_id != str(user_id):
|
||||
raise HTTPException(status_code=404, detail="Adventure not found")
|
||||
|
||||
entries = await PostgresAdventureEntryRepository(db).list_for_adventure(adv_id)
|
||||
|
||||
translation_repo = PostgresAdventureEntryTranslationRepository(db)
|
||||
audio_repo = PostgresAdventureEntryAudioRepository(db)
|
||||
|
||||
entry_items = []
|
||||
for entry in entries:
|
||||
eid = uuid.UUID(entry.id)
|
||||
translation = await translation_repo.get_for_entry(
|
||||
entry_id=eid,
|
||||
component_type="story_text",
|
||||
target_language=adventure.source_language,
|
||||
)
|
||||
audio = await audio_repo.get_for_entry(entry_id=eid, component_type="story_text")
|
||||
entry_items.append(
|
||||
AdventureEntryItem(
|
||||
id=entry.id,
|
||||
adventure_id=entry.adventure_id,
|
||||
generated_from_choice_id=entry.generated_from_choice_id,
|
||||
status=entry.status,
|
||||
entry_index=entry.entry_index,
|
||||
story_text=entry.story_text,
|
||||
translation=translation.translated_text if translation else None,
|
||||
audio_url=_audio_url(audio.file_name if audio else None),
|
||||
created_at=entry.created_at.isoformat(),
|
||||
)
|
||||
)
|
||||
|
||||
# Choices for the most recent entry, if it's complete (entries are ordered entry_index asc)
|
||||
current_entry_choices: list[AdventureChoiceItem] = []
|
||||
if entries and entries[-1].status == "complete":
|
||||
choices = await PostgresAdventureEntryChoiceRepository(db).list_for_entry(
|
||||
uuid.UUID(entries[-1].id)
|
||||
)
|
||||
current_entry_choices = [
|
||||
AdventureChoiceItem(id=c.id, index=c.index, label=c.label, text=c.text)
|
||||
for c in choices
|
||||
]
|
||||
|
||||
return AdventureDetailResponse(
|
||||
id=adventure.id,
|
||||
user_id=adventure.user_id,
|
||||
status=adventure.status,
|
||||
language=adventure.language,
|
||||
source_language=adventure.source_language,
|
||||
competencies=adventure.competencies,
|
||||
max_entry_count=adventure.max_entry_count,
|
||||
title=adventure.title,
|
||||
description=adventure.description,
|
||||
genres=adventure.genres,
|
||||
setting=adventure.setting,
|
||||
vibes=adventure.vibes,
|
||||
protagonist=adventure.protagonist,
|
||||
created_at=adventure.created_at.isoformat(),
|
||||
entries=entry_items,
|
||||
current_entry_choices=current_entry_choices,
|
||||
)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from .account import router as account_router
|
||||
from .adventure import router as adventure_router
|
||||
from .articles import router as article_router
|
||||
from .user_profile import router as user_profile_router
|
||||
from .packs import router as packs_router
|
||||
|
|
@ -8,6 +9,7 @@ from fastapi import APIRouter
|
|||
bff_router = APIRouter(prefix="/bff", tags=["bff"])
|
||||
|
||||
bff_router.include_router(account_router)
|
||||
bff_router.include_router(adventure_router)
|
||||
bff_router.include_router(article_router)
|
||||
bff_router.include_router(user_profile_router)
|
||||
bff_router.include_router(packs_router)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -192,6 +192,140 @@ export type AddWordRequest = {
|
|||
source_article_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* AdventureChoiceItem
|
||||
*/
|
||||
export type AdventureChoiceItem = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Index
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* Label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Text
|
||||
*/
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* AdventureDetailResponse
|
||||
*/
|
||||
export type AdventureDetailResponse = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* User Id
|
||||
*/
|
||||
user_id: string;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Language
|
||||
*/
|
||||
language: string;
|
||||
/**
|
||||
* Source Language
|
||||
*/
|
||||
source_language: string;
|
||||
/**
|
||||
* Competencies
|
||||
*/
|
||||
competencies: Array<string>;
|
||||
/**
|
||||
* Max Entry Count
|
||||
*/
|
||||
max_entry_count: number;
|
||||
/**
|
||||
* Title
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Genres
|
||||
*/
|
||||
genres: Array<string>;
|
||||
/**
|
||||
* Setting
|
||||
*/
|
||||
setting: Array<string>;
|
||||
/**
|
||||
* Vibes
|
||||
*/
|
||||
vibes: Array<string>;
|
||||
/**
|
||||
* Protagonist
|
||||
*/
|
||||
protagonist: Array<string>;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* Entries
|
||||
*/
|
||||
entries: Array<AdventureEntryItem>;
|
||||
/**
|
||||
* Current Entry Choices
|
||||
*/
|
||||
current_entry_choices: Array<AdventureChoiceItem>;
|
||||
};
|
||||
|
||||
/**
|
||||
* AdventureEntryItem
|
||||
*/
|
||||
export type AdventureEntryItem = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Adventure Id
|
||||
*/
|
||||
adventure_id: string;
|
||||
/**
|
||||
* Generated From Choice Id
|
||||
*/
|
||||
generated_from_choice_id: string | null;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Entry Index
|
||||
*/
|
||||
entry_index: number;
|
||||
/**
|
||||
* Story Text
|
||||
*/
|
||||
story_text: string | null;
|
||||
/**
|
||||
* Translation
|
||||
*/
|
||||
translation: string | null;
|
||||
/**
|
||||
* Audio Url
|
||||
*/
|
||||
audio_url: string | null;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* AdventureResponse
|
||||
*/
|
||||
|
|
@ -2893,6 +3027,36 @@ export type GetOnboardingBffAccountOnboardingGetResponses = {
|
|||
|
||||
export type GetOnboardingBffAccountOnboardingGetResponse = GetOnboardingBffAccountOnboardingGetResponses[keyof GetOnboardingBffAccountOnboardingGetResponses];
|
||||
|
||||
export type GetAdventureBffAdventureAdventureIdGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Adventure Id
|
||||
*/
|
||||
adventure_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/bff/adventure/{adventure_id}';
|
||||
};
|
||||
|
||||
export type GetAdventureBffAdventureAdventureIdGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetAdventureBffAdventureAdventureIdGetError = GetAdventureBffAdventureAdventureIdGetErrors[keyof GetAdventureBffAdventureAdventureIdGetErrors];
|
||||
|
||||
export type GetAdventureBffAdventureAdventureIdGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: AdventureDetailResponse;
|
||||
};
|
||||
|
||||
export type GetAdventureBffAdventureAdventureIdGetResponse = GetAdventureBffAdventureAdventureIdGetResponses[keyof GetAdventureBffAdventureAdventureIdGetResponses];
|
||||
|
||||
export type ListArticlesBffArticlesGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
|
|
|||
Loading…
Reference in a new issue