refactor(import): simplify API failure handling
- rely on ky for transport errors across import/staging calls - keep explicit soft-failure checks for auto-import approval endpoints - add regression test for approval/rejection soft failures
This commit is contained in:
parent
d32d88ea0e
commit
9f2c5c685b
2 changed files with 103 additions and 132 deletions
29
webui/src/routes/import/-import.api.test.ts
Normal file
29
webui/src/routes/import/-import.api.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { HttpResponse, http, server } from '@/test/msw';
|
||||
|
||||
import { approveAutoImportResult, rejectAutoImportResult } from './-import.api';
|
||||
|
||||
const softFailureMessage = 'Item not found or not pending review';
|
||||
|
||||
describe('import api', () => {
|
||||
it('surfaces soft failures from auto-import approval endpoints', async () => {
|
||||
server.use(
|
||||
http.post('/api/auto-import/approve/17', () =>
|
||||
HttpResponse.json({
|
||||
success: false,
|
||||
error: softFailureMessage,
|
||||
}),
|
||||
),
|
||||
http.post('/api/auto-import/reject/18', () =>
|
||||
HttpResponse.json({
|
||||
success: false,
|
||||
error: softFailureMessage,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(approveAutoImportResult(17)).rejects.toThrow(softFailureMessage);
|
||||
await expect(rejectAutoImportResult(18)).rejects.toThrow(softFailureMessage);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,48 +18,26 @@ import type {
|
|||
|
||||
export const IMPORT_QUERY_KEY = ['import'] as const;
|
||||
|
||||
function assertSuccess<T extends { success: boolean; error?: string }>(
|
||||
payload: T,
|
||||
fallback: string,
|
||||
): T {
|
||||
if (!payload.success) {
|
||||
throw new Error(payload.error || fallback);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function fetchImportStagingFiles(): Promise<ImportStagingFilesPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files')),
|
||||
'Failed to load import folder',
|
||||
);
|
||||
return readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files'));
|
||||
}
|
||||
|
||||
export async function fetchImportStagingGroups(): Promise<ImportStagingGroupsPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportStagingGroupsPayload>(apiClient.get('import/staging/groups')),
|
||||
'Failed to load auto-detected albums',
|
||||
);
|
||||
return readJson<ImportStagingGroupsPayload>(apiClient.get('import/staging/groups'));
|
||||
}
|
||||
|
||||
export async function fetchImportStagingSuggestions(): Promise<ImportAlbumSearchPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAlbumSearchPayload>(apiClient.get('import/staging/suggestions')),
|
||||
'Failed to load import suggestions',
|
||||
);
|
||||
return readJson<ImportAlbumSearchPayload>(apiClient.get('import/staging/suggestions'));
|
||||
}
|
||||
|
||||
export async function searchImportAlbums(query: string): Promise<ImportAlbumSearchPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAlbumSearchPayload>(
|
||||
apiClient.get('import/search/albums', {
|
||||
searchParams: {
|
||||
q: query,
|
||||
limit: '12',
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Album search failed',
|
||||
return readJson<ImportAlbumSearchPayload>(
|
||||
apiClient.get('import/search/albums', {
|
||||
searchParams: {
|
||||
q: query,
|
||||
limit: '12',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -70,19 +48,16 @@ export async function matchImportAlbum(input: {
|
|||
albumArtist?: string | null;
|
||||
filePaths?: string[] | null;
|
||||
}): Promise<ImportAlbumMatchPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAlbumMatchPayload>(
|
||||
apiClient.post('import/album/match', {
|
||||
json: {
|
||||
album_id: input.albumId,
|
||||
source: input.source || '',
|
||||
album_name: input.albumName || '',
|
||||
album_artist: input.albumArtist || '',
|
||||
...(input.filePaths?.length ? { file_paths: input.filePaths } : {}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Failed to match album',
|
||||
return readJson<ImportAlbumMatchPayload>(
|
||||
apiClient.post('import/album/match', {
|
||||
json: {
|
||||
album_id: input.albumId,
|
||||
source: input.source || '',
|
||||
album_name: input.albumName || '',
|
||||
album_artist: input.albumArtist || '',
|
||||
...(input.filePaths?.length ? { file_paths: input.filePaths } : {}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,142 +65,109 @@ export async function processImportAlbumTrack(input: {
|
|||
album: ImportAlbum;
|
||||
match: ImportAlbumMatch;
|
||||
}): Promise<ImportProcessPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportProcessPayload>(
|
||||
apiClient.post('import/album/process', {
|
||||
json: {
|
||||
album: input.album,
|
||||
matches: [input.match],
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Failed to process album track',
|
||||
return readJson<ImportProcessPayload>(
|
||||
apiClient.post('import/album/process', {
|
||||
json: {
|
||||
album: input.album,
|
||||
matches: [input.match],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function searchImportTracks(query: string): Promise<ImportTrackSearchPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportTrackSearchPayload>(
|
||||
apiClient.get('import/search/tracks', {
|
||||
searchParams: {
|
||||
q: query,
|
||||
limit: '6',
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Track search failed',
|
||||
return readJson<ImportTrackSearchPayload>(
|
||||
apiClient.get('import/search/tracks', {
|
||||
searchParams: {
|
||||
q: query,
|
||||
limit: '6',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function processImportSingleFile(file: unknown): Promise<ImportProcessPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportProcessPayload>(
|
||||
apiClient.post('import/singles/process', {
|
||||
json: {
|
||||
files: [file],
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Failed to process single',
|
||||
return readJson<ImportProcessPayload>(
|
||||
apiClient.post('import/singles/process', {
|
||||
json: {
|
||||
files: [file],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAutoImportStatus(): Promise<ImportAutoImportStatusPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAutoImportStatusPayload>(apiClient.get('auto-import/status')),
|
||||
'Failed to load auto-import status',
|
||||
);
|
||||
return readJson<ImportAutoImportStatusPayload>(apiClient.get('auto-import/status'));
|
||||
}
|
||||
|
||||
export async function fetchAutoImportSettings(): Promise<ImportAutoImportSettingsPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAutoImportSettingsPayload>(apiClient.get('auto-import/settings')),
|
||||
'Failed to load auto-import settings',
|
||||
);
|
||||
return readJson<ImportAutoImportSettingsPayload>(apiClient.get('auto-import/settings'));
|
||||
}
|
||||
|
||||
export async function saveAutoImportSettings(input: {
|
||||
confidenceThreshold: number;
|
||||
scanInterval: number;
|
||||
}): Promise<void> {
|
||||
assertSuccess(
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post('auto-import/settings', {
|
||||
json: {
|
||||
confidence_threshold: input.confidenceThreshold,
|
||||
scan_interval: input.scanInterval,
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Failed to save auto-import settings',
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post('auto-import/settings', {
|
||||
json: {
|
||||
confidence_threshold: input.confidenceThreshold,
|
||||
scan_interval: input.scanInterval,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAutoImportResults(): Promise<ImportAutoImportResultsPayload> {
|
||||
return assertSuccess(
|
||||
await readJson<ImportAutoImportResultsPayload>(
|
||||
apiClient.get('auto-import/results', {
|
||||
searchParams: {
|
||||
limit: '100',
|
||||
},
|
||||
}),
|
||||
),
|
||||
'Failed to load auto-import results',
|
||||
return readJson<ImportAutoImportResultsPayload>(
|
||||
apiClient.get('auto-import/results', {
|
||||
searchParams: {
|
||||
limit: '100',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function toggleAutoImport(enabled: boolean): Promise<void> {
|
||||
assertSuccess(
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post('auto-import/toggle', {
|
||||
json: { enabled },
|
||||
}),
|
||||
),
|
||||
'Failed to toggle auto-import',
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post('auto-import/toggle', {
|
||||
json: { enabled },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function triggerAutoImportScan(): Promise<void> {
|
||||
assertSuccess(
|
||||
await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now')),
|
||||
'Failed to trigger scan',
|
||||
);
|
||||
await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now'));
|
||||
}
|
||||
|
||||
export async function approveAutoImportResult(id: number): Promise<void> {
|
||||
assertSuccess(
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post(`auto-import/approve/${id}`),
|
||||
),
|
||||
'Failed to approve import',
|
||||
const payload = await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post(`auto-import/approve/${id}`),
|
||||
);
|
||||
if (!payload.success) {
|
||||
throw new Error(payload.error || 'Failed to approve import');
|
||||
}
|
||||
}
|
||||
|
||||
export async function rejectAutoImportResult(id: number): Promise<void> {
|
||||
assertSuccess(
|
||||
await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post(`auto-import/reject/${id}`),
|
||||
),
|
||||
'Failed to dismiss import',
|
||||
const payload = await readJson<{ success: boolean; error?: string }>(
|
||||
apiClient.post(`auto-import/reject/${id}`),
|
||||
);
|
||||
if (!payload.success) {
|
||||
throw new Error(payload.error || 'Failed to dismiss import');
|
||||
}
|
||||
}
|
||||
|
||||
export async function approveAllAutoImportResults(): Promise<number> {
|
||||
const payload = assertSuccess(
|
||||
await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||
apiClient.post('auto-import/approve-all'),
|
||||
),
|
||||
'Failed to approve imports',
|
||||
const payload = await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||
apiClient.post('auto-import/approve-all'),
|
||||
);
|
||||
return payload.count ?? 0;
|
||||
}
|
||||
|
||||
export async function clearCompletedAutoImportResults(): Promise<number> {
|
||||
const payload = assertSuccess(
|
||||
await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||
apiClient.post('auto-import/clear-completed'),
|
||||
),
|
||||
'Failed to clear import history',
|
||||
const payload = await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||
apiClient.post('auto-import/clear-completed'),
|
||||
);
|
||||
return payload.count ?? 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue