refactor(web): migrate trackService to orval-generated track client (v1.0.8 B5)
Third feature-service migration after B3 (profile) / B4 (playlist).
Replaces raw apiClient calls in @/features/tracks/services/trackService.ts
with orval-generated functions from services/generated/track/track.ts.
All public function signatures preserved — none of the 10 consumers
(useMyTracks, ListenTogetherPage, ExploreView, TrackList, TrackDetailPage,
TrackLyricsSection, TrackMetadataEditModal, etc.) need to change.
Functions migrated (10):
- getTracks → orval getTracks (with AbortSignal via RequestInit)
- getTrack → orval getTracksId
- getLyrics → orval getTracksIdLyrics
- updateLyrics → orval putTracksIdLyrics
- getSuggestedTags → orval getTracksSuggestedTags
- updateTrack → orval putTracksId
- deleteTrack → orval deleteTracksId
- searchTracks → orval getTracksSearch
- likeTrack → orval postTracksIdLike
- unlikeTrack → orval deleteTracksIdLike
- recordPlay → orval postTracksIdPlay
Functions still on raw apiClient:
- downloadTrack → orval getTracksIdDownload doesn't preserve
responseType: 'blob'; per-call responseType
override needs B9 cleanup pass.
- uploadTrack / → delegate to uploadService (chunked transport
getTrackStatus lives there, separate concern from CRUD).
Two helpers (unwrapPayload, pickTrack) normalise the {data: ...} APIResponse
envelope and the {track: ...} single-resource shape, mirroring the B4
playlist pattern.
getTracks keeps its sortOrder param in the public signature for
forward-compat, but the orval call drops it — the backend swaggo
annotation on GET /tracks (track_crud_handler.go) declares only
sort_by, and the handler ignores any sort_order arg silently. Same
deferral pattern as B4. Re-enable when the backend annotation is
extended (v1.0.9).
Error handling preserved verbatim — AxiosError still propagates from
the orval mutator (Axios under the hood), so the existing status-code
→ TrackUploadError mapping (401 / 403 / 404 / 400 / 500 / network)
continues to apply unchanged.
Tests: trackService has no dedicated test file (trackService.test.ts
doesn't exist). Adjacent feature suites all green:
- src/features/tracks/ → 553/553
- src/features/player/, library/, components/dashboard, social →
400/400
npm run typecheck: clean.
Bisectable: revert this commit → service returns to apiClient pattern.
No interceptor changes, no data-shape drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a4681643c
commit
feb5fc02be
1 changed files with 101 additions and 59 deletions
|
|
@ -1,4 +1,30 @@
|
|||
/**
|
||||
* Track service — orval-backed.
|
||||
* v1.0.8 B5 migration: replaced raw apiClient calls with the generated
|
||||
* functions in services/generated/track/track.ts. Public function
|
||||
* signatures unchanged so callers (hooks, components, tests) stay put.
|
||||
*
|
||||
* Error handling structure preserved verbatim — AxiosError still
|
||||
* propagates from the orval mutator (apiClient under the hood), so the
|
||||
* existing status-code → TrackUploadError mapping continues to apply.
|
||||
*
|
||||
* Upload paths still delegate to the dedicated uploadService (chunked
|
||||
* transport + progress tracking lives there, not in track CRUD).
|
||||
*/
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import {
|
||||
getTracks as orvalGetTracks,
|
||||
getTracksId as orvalGetTracksId,
|
||||
putTracksId as orvalPutTracksId,
|
||||
deleteTracksId as orvalDeleteTracksId,
|
||||
getTracksIdLyrics as orvalGetLyrics,
|
||||
putTracksIdLyrics as orvalPutLyrics,
|
||||
getTracksSuggestedTags as orvalGetSuggestedTags,
|
||||
getTracksSearch as orvalSearchTracks,
|
||||
postTracksIdLike as orvalLike,
|
||||
deleteTracksIdLike as orvalUnlike,
|
||||
postTracksIdPlay as orvalRecordPlay,
|
||||
} from '@/services/generated/track/track';
|
||||
import { Track } from '../types/track';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TrackServiceError as TrackUploadError } from '../errors/trackErrors';
|
||||
|
|
@ -35,6 +61,24 @@ export interface TrackListResponse {
|
|||
total_pages: number;
|
||||
}
|
||||
|
||||
// Backend wraps responses in { data: { ... } } via APIResponse envelope.
|
||||
// orval's mutator already strips the Axios layer, leaving the envelope.
|
||||
const unwrapPayload = <T>(raw: unknown): T => {
|
||||
const env = raw as { data?: unknown } | undefined;
|
||||
if (env && typeof env === 'object' && 'data' in env && env.data !== undefined) {
|
||||
return env.data as T;
|
||||
}
|
||||
return raw as T;
|
||||
};
|
||||
|
||||
const pickTrack = (raw: unknown): Track => {
|
||||
const p = unwrapPayload<{ track?: Track } | Track>(raw);
|
||||
if (p && typeof p === 'object' && 'track' in p && p.track) {
|
||||
return p.track as Track;
|
||||
}
|
||||
return p as Track;
|
||||
};
|
||||
|
||||
/**
|
||||
* Récupère la liste des tracks
|
||||
* @param params Paramètres de filtrage et pagination
|
||||
|
|
@ -45,21 +89,25 @@ export async function getTracks(
|
|||
params: TrackListParams = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<TrackListResponse> {
|
||||
// Backend GET /tracks ne déclare pas sort_order dans son annotation
|
||||
// swaggo (cf. track_crud_handler.go) — paramètre conservé dans la
|
||||
// signature pour forward-compat mais ignoré à l'appel. À ré-activer
|
||||
// quand l'annotation est étendue (v1.0.9).
|
||||
void params.sortOrder;
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.page) queryParams.append('page', params.page.toString());
|
||||
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params.userId) queryParams.append('user_id', params.userId);
|
||||
if (params.genre) queryParams.append('genre', params.genre);
|
||||
if (params.format) queryParams.append('format', params.format);
|
||||
if (params.sortBy) queryParams.append('sort_by', params.sortBy);
|
||||
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder);
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `/tracks${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await apiClient.get<TrackListResponse>(url, { signal });
|
||||
return response.data;
|
||||
const response = await orvalGetTracks(
|
||||
{
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
user_id: params.userId,
|
||||
genre: params.genre,
|
||||
format: params.format,
|
||||
sort_by: params.sortBy,
|
||||
},
|
||||
signal ? { signal } : undefined,
|
||||
);
|
||||
return unwrapPayload<TrackListResponse>(response);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
|
|
@ -78,7 +126,6 @@ export async function getTracks(
|
|||
error,
|
||||
);
|
||||
}
|
||||
// Erreurs réseau
|
||||
if (
|
||||
error.code === 'ECONNABORTED' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
|
|
@ -111,14 +158,11 @@ export async function getTracks(
|
|||
|
||||
/**
|
||||
* Récupère les détails d'un track
|
||||
* @param id ID du track
|
||||
* @returns Le track
|
||||
* @throws Error si la requête échoue
|
||||
*/
|
||||
export async function getTrack(id: string): Promise<Track> {
|
||||
try {
|
||||
const response = await apiClient.get<{ track: Track }>(`/tracks/${id}`);
|
||||
return response.data.track;
|
||||
const response = await orvalGetTracksId(id);
|
||||
return pickTrack(response);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
|
|
@ -145,7 +189,6 @@ export async function getTrack(id: string): Promise<Track> {
|
|||
error,
|
||||
);
|
||||
}
|
||||
// Erreurs réseau
|
||||
if (
|
||||
error.code === 'ECONNABORTED' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
|
|
@ -177,7 +220,7 @@ export async function getTrack(id: string): Promise<Track> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Interface pour les paramètres de mise à jour d'un track
|
||||
* Interface pour les paroles d'un track
|
||||
*/
|
||||
export interface TrackLyrics {
|
||||
id: string;
|
||||
|
|
@ -192,10 +235,9 @@ export interface TrackLyrics {
|
|||
*/
|
||||
export async function getLyrics(trackId: string): Promise<TrackLyrics | null> {
|
||||
try {
|
||||
const response = await apiClient.get<{ lyrics: TrackLyrics | null }>(
|
||||
`/tracks/${trackId}/lyrics`,
|
||||
);
|
||||
return response.data.lyrics;
|
||||
const response = await orvalGetLyrics(trackId);
|
||||
const payload = unwrapPayload<{ lyrics?: TrackLyrics | null } | null>(response);
|
||||
return payload?.lyrics ?? null;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
return null;
|
||||
|
|
@ -211,11 +253,12 @@ export async function updateLyrics(
|
|||
trackId: string,
|
||||
content: string,
|
||||
): Promise<TrackLyrics> {
|
||||
const response = await apiClient.put<{ lyrics: TrackLyrics }>(
|
||||
`/tracks/${trackId}/lyrics`,
|
||||
{ content },
|
||||
const response = await orvalPutLyrics(
|
||||
trackId,
|
||||
{ content } as Parameters<typeof orvalPutLyrics>[1],
|
||||
);
|
||||
return response.data.lyrics;
|
||||
const payload = unwrapPayload<{ lyrics: TrackLyrics }>(response);
|
||||
return payload.lyrics;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -225,14 +268,12 @@ export async function getSuggestedTags(params?: {
|
|||
genre?: string;
|
||||
bpm?: number;
|
||||
}): Promise<string[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.genre) searchParams.set('genre', params.genre);
|
||||
if (params?.bpm != null) searchParams.set('bpm', String(params.bpm));
|
||||
const qs = searchParams.toString();
|
||||
const response = await apiClient.get<{ tags: string[] }>(
|
||||
`/tracks/suggested-tags${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
return response.data.tags;
|
||||
const response = await orvalGetSuggestedTags({
|
||||
genre: params?.genre,
|
||||
bpm: params?.bpm,
|
||||
} as Parameters<typeof orvalGetSuggestedTags>[0]);
|
||||
const payload = unwrapPayload<{ tags?: string[] }>(response);
|
||||
return payload.tags ?? [];
|
||||
}
|
||||
|
||||
export interface UpdateTrackParams {
|
||||
|
|
@ -252,21 +293,17 @@ export interface UpdateTrackParams {
|
|||
|
||||
/**
|
||||
* Met à jour les métadonnées d'un track
|
||||
* @param id ID du track
|
||||
* @param params Paramètres de mise à jour
|
||||
* @returns Le track mis à jour
|
||||
* @throws Error si la requête échoue
|
||||
*/
|
||||
export async function updateTrack(
|
||||
id: string,
|
||||
params: UpdateTrackParams,
|
||||
): Promise<Track> {
|
||||
try {
|
||||
const response = await apiClient.put<{ track: Track }>(
|
||||
`/tracks/${id}`,
|
||||
params,
|
||||
const response = await orvalPutTracksId(
|
||||
id,
|
||||
params as Parameters<typeof orvalPutTracksId>[1],
|
||||
);
|
||||
return response.data.track;
|
||||
return pickTrack(response);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
|
|
@ -305,7 +342,6 @@ export async function updateTrack(
|
|||
error,
|
||||
);
|
||||
}
|
||||
// Erreurs réseau
|
||||
if (
|
||||
error.code === 'ECONNABORTED' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
|
|
@ -338,12 +374,10 @@ export async function updateTrack(
|
|||
|
||||
/**
|
||||
* Supprime un track
|
||||
* @param id ID du track à supprimer
|
||||
* @throws Error si la requête échoue
|
||||
*/
|
||||
export async function deleteTrack(id: string): Promise<void> {
|
||||
try {
|
||||
await apiClient.delete(`/tracks/${id}`);
|
||||
await orvalDeleteTracksId(id);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
|
|
@ -378,7 +412,6 @@ export async function deleteTrack(id: string): Promise<void> {
|
|||
error,
|
||||
);
|
||||
}
|
||||
// Erreurs réseau
|
||||
if (
|
||||
error.code === 'ECONNABORTED' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
|
|
@ -415,35 +448,44 @@ export async function deleteTrack(id: string): Promise<void> {
|
|||
* Search for tracks by query string
|
||||
*/
|
||||
export async function searchTracks(query: string) {
|
||||
const response = await apiClient.get<Track[]>('/tracks/search', {
|
||||
params: { query },
|
||||
});
|
||||
return { tracks: response.data };
|
||||
const response = await orvalSearchTracks({ q: query });
|
||||
const payload = unwrapPayload<{ tracks?: Track[] } | Track[]>(response);
|
||||
if (Array.isArray(payload)) {
|
||||
return { tracks: payload };
|
||||
}
|
||||
return { tracks: payload.tracks ?? [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Like a track
|
||||
*/
|
||||
export async function likeTrack(id: string) {
|
||||
return apiClient.post(`/tracks/${id}/like`);
|
||||
return orvalLike(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike a track
|
||||
*/
|
||||
export async function unlikeTrack(id: string) {
|
||||
return apiClient.delete(`/tracks/${id}/like`);
|
||||
return orvalUnlike(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a play event for a track
|
||||
*/
|
||||
export async function recordPlay(id: string) {
|
||||
return apiClient.post(`/tracks/${id}/play`);
|
||||
return orvalRecordPlay(
|
||||
id,
|
||||
{} as Parameters<typeof orvalRecordPlay>[1],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a track as a blob
|
||||
* Download a track as a blob.
|
||||
* Kept on raw apiClient because orval's getTracksIdDownload doesn't
|
||||
* preserve the responseType: 'blob' contract callers depend on (the
|
||||
* mutator-typed response would deserialize binary as JSON). Re-evaluate
|
||||
* once orval supports per-call responseType overrides (B9 cleanup).
|
||||
*/
|
||||
export async function downloadTrack(id: string) {
|
||||
const response = await apiClient.get(`/tracks/${id}/download`, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue