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;
|
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> {
|
export async function fetchImportStagingFiles(): Promise<ImportStagingFilesPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files'));
|
||||||
await readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files')),
|
|
||||||
'Failed to load import folder',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchImportStagingGroups(): Promise<ImportStagingGroupsPayload> {
|
export async function fetchImportStagingGroups(): Promise<ImportStagingGroupsPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportStagingGroupsPayload>(apiClient.get('import/staging/groups'));
|
||||||
await readJson<ImportStagingGroupsPayload>(apiClient.get('import/staging/groups')),
|
|
||||||
'Failed to load auto-detected albums',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchImportStagingSuggestions(): Promise<ImportAlbumSearchPayload> {
|
export async function fetchImportStagingSuggestions(): Promise<ImportAlbumSearchPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAlbumSearchPayload>(apiClient.get('import/staging/suggestions'));
|
||||||
await readJson<ImportAlbumSearchPayload>(apiClient.get('import/staging/suggestions')),
|
|
||||||
'Failed to load import suggestions',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchImportAlbums(query: string): Promise<ImportAlbumSearchPayload> {
|
export async function searchImportAlbums(query: string): Promise<ImportAlbumSearchPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAlbumSearchPayload>(
|
||||||
await readJson<ImportAlbumSearchPayload>(
|
apiClient.get('import/search/albums', {
|
||||||
apiClient.get('import/search/albums', {
|
searchParams: {
|
||||||
searchParams: {
|
q: query,
|
||||||
q: query,
|
limit: '12',
|
||||||
limit: '12',
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Album search failed',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,19 +48,16 @@ export async function matchImportAlbum(input: {
|
||||||
albumArtist?: string | null;
|
albumArtist?: string | null;
|
||||||
filePaths?: string[] | null;
|
filePaths?: string[] | null;
|
||||||
}): Promise<ImportAlbumMatchPayload> {
|
}): Promise<ImportAlbumMatchPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAlbumMatchPayload>(
|
||||||
await readJson<ImportAlbumMatchPayload>(
|
apiClient.post('import/album/match', {
|
||||||
apiClient.post('import/album/match', {
|
json: {
|
||||||
json: {
|
album_id: input.albumId,
|
||||||
album_id: input.albumId,
|
source: input.source || '',
|
||||||
source: input.source || '',
|
album_name: input.albumName || '',
|
||||||
album_name: input.albumName || '',
|
album_artist: input.albumArtist || '',
|
||||||
album_artist: input.albumArtist || '',
|
...(input.filePaths?.length ? { file_paths: input.filePaths } : {}),
|
||||||
...(input.filePaths?.length ? { file_paths: input.filePaths } : {}),
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to match album',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,142 +65,109 @@ export async function processImportAlbumTrack(input: {
|
||||||
album: ImportAlbum;
|
album: ImportAlbum;
|
||||||
match: ImportAlbumMatch;
|
match: ImportAlbumMatch;
|
||||||
}): Promise<ImportProcessPayload> {
|
}): Promise<ImportProcessPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportProcessPayload>(
|
||||||
await readJson<ImportProcessPayload>(
|
apiClient.post('import/album/process', {
|
||||||
apiClient.post('import/album/process', {
|
json: {
|
||||||
json: {
|
album: input.album,
|
||||||
album: input.album,
|
matches: [input.match],
|
||||||
matches: [input.match],
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to process album track',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchImportTracks(query: string): Promise<ImportTrackSearchPayload> {
|
export async function searchImportTracks(query: string): Promise<ImportTrackSearchPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportTrackSearchPayload>(
|
||||||
await readJson<ImportTrackSearchPayload>(
|
apiClient.get('import/search/tracks', {
|
||||||
apiClient.get('import/search/tracks', {
|
searchParams: {
|
||||||
searchParams: {
|
q: query,
|
||||||
q: query,
|
limit: '6',
|
||||||
limit: '6',
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Track search failed',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processImportSingleFile(file: unknown): Promise<ImportProcessPayload> {
|
export async function processImportSingleFile(file: unknown): Promise<ImportProcessPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportProcessPayload>(
|
||||||
await readJson<ImportProcessPayload>(
|
apiClient.post('import/singles/process', {
|
||||||
apiClient.post('import/singles/process', {
|
json: {
|
||||||
json: {
|
files: [file],
|
||||||
files: [file],
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to process single',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAutoImportStatus(): Promise<ImportAutoImportStatusPayload> {
|
export async function fetchAutoImportStatus(): Promise<ImportAutoImportStatusPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAutoImportStatusPayload>(apiClient.get('auto-import/status'));
|
||||||
await readJson<ImportAutoImportStatusPayload>(apiClient.get('auto-import/status')),
|
|
||||||
'Failed to load auto-import status',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAutoImportSettings(): Promise<ImportAutoImportSettingsPayload> {
|
export async function fetchAutoImportSettings(): Promise<ImportAutoImportSettingsPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAutoImportSettingsPayload>(apiClient.get('auto-import/settings'));
|
||||||
await readJson<ImportAutoImportSettingsPayload>(apiClient.get('auto-import/settings')),
|
|
||||||
'Failed to load auto-import settings',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAutoImportSettings(input: {
|
export async function saveAutoImportSettings(input: {
|
||||||
confidenceThreshold: number;
|
confidenceThreshold: number;
|
||||||
scanInterval: number;
|
scanInterval: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
assertSuccess(
|
await readJson<{ success: boolean; error?: string }>(
|
||||||
await readJson<{ success: boolean; error?: string }>(
|
apiClient.post('auto-import/settings', {
|
||||||
apiClient.post('auto-import/settings', {
|
json: {
|
||||||
json: {
|
confidence_threshold: input.confidenceThreshold,
|
||||||
confidence_threshold: input.confidenceThreshold,
|
scan_interval: input.scanInterval,
|
||||||
scan_interval: input.scanInterval,
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to save auto-import settings',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAutoImportResults(): Promise<ImportAutoImportResultsPayload> {
|
export async function fetchAutoImportResults(): Promise<ImportAutoImportResultsPayload> {
|
||||||
return assertSuccess(
|
return readJson<ImportAutoImportResultsPayload>(
|
||||||
await readJson<ImportAutoImportResultsPayload>(
|
apiClient.get('auto-import/results', {
|
||||||
apiClient.get('auto-import/results', {
|
searchParams: {
|
||||||
searchParams: {
|
limit: '100',
|
||||||
limit: '100',
|
},
|
||||||
},
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to load auto-import results',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleAutoImport(enabled: boolean): Promise<void> {
|
export async function toggleAutoImport(enabled: boolean): Promise<void> {
|
||||||
assertSuccess(
|
await readJson<{ success: boolean; error?: string }>(
|
||||||
await readJson<{ success: boolean; error?: string }>(
|
apiClient.post('auto-import/toggle', {
|
||||||
apiClient.post('auto-import/toggle', {
|
json: { enabled },
|
||||||
json: { enabled },
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
'Failed to toggle auto-import',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function triggerAutoImportScan(): Promise<void> {
|
export async function triggerAutoImportScan(): Promise<void> {
|
||||||
assertSuccess(
|
await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now'));
|
||||||
await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now')),
|
|
||||||
'Failed to trigger scan',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function approveAutoImportResult(id: number): Promise<void> {
|
export async function approveAutoImportResult(id: number): Promise<void> {
|
||||||
assertSuccess(
|
const payload = await readJson<{ success: boolean; error?: string }>(
|
||||||
await readJson<{ success: boolean; error?: string }>(
|
apiClient.post(`auto-import/approve/${id}`),
|
||||||
apiClient.post(`auto-import/approve/${id}`),
|
|
||||||
),
|
|
||||||
'Failed to approve import',
|
|
||||||
);
|
);
|
||||||
|
if (!payload.success) {
|
||||||
|
throw new Error(payload.error || 'Failed to approve import');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rejectAutoImportResult(id: number): Promise<void> {
|
export async function rejectAutoImportResult(id: number): Promise<void> {
|
||||||
assertSuccess(
|
const payload = await readJson<{ success: boolean; error?: string }>(
|
||||||
await readJson<{ success: boolean; error?: string }>(
|
apiClient.post(`auto-import/reject/${id}`),
|
||||||
apiClient.post(`auto-import/reject/${id}`),
|
|
||||||
),
|
|
||||||
'Failed to dismiss import',
|
|
||||||
);
|
);
|
||||||
|
if (!payload.success) {
|
||||||
|
throw new Error(payload.error || 'Failed to dismiss import');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function approveAllAutoImportResults(): Promise<number> {
|
export async function approveAllAutoImportResults(): Promise<number> {
|
||||||
const payload = assertSuccess(
|
const payload = await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||||
await readJson<{ success: boolean; count?: number; error?: string }>(
|
apiClient.post('auto-import/approve-all'),
|
||||||
apiClient.post('auto-import/approve-all'),
|
|
||||||
),
|
|
||||||
'Failed to approve imports',
|
|
||||||
);
|
);
|
||||||
return payload.count ?? 0;
|
return payload.count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearCompletedAutoImportResults(): Promise<number> {
|
export async function clearCompletedAutoImportResults(): Promise<number> {
|
||||||
const payload = assertSuccess(
|
const payload = await readJson<{ success: boolean; count?: number; error?: string }>(
|
||||||
await readJson<{ success: boolean; count?: number; error?: string }>(
|
apiClient.post('auto-import/clear-completed'),
|
||||||
apiClient.post('auto-import/clear-completed'),
|
|
||||||
),
|
|
||||||
'Failed to clear import history',
|
|
||||||
);
|
);
|
||||||
return payload.count ?? 0;
|
return payload.count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue