feat(frontend): add /documents page.server.ts — search load function with all filter params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
169
frontend/src/routes/documents/+page.server.spec.ts
Normal file
169
frontend/src/routes/documents/+page.server.spec.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('$lib/api.server', () => ({ createApiClient: vi.fn() }));
|
||||||
|
|
||||||
|
import { load } from './+page.server';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
|
||||||
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
|
function makeUrl(params: Record<string, string | string[]> = {}) {
|
||||||
|
const url = new URL('http://localhost/documents');
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => url.searchParams.append(key, v));
|
||||||
|
} else {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── search params forwarding ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('documents page load — search params', () => {
|
||||||
|
it('passes q, from, to to the search API', async () => {
|
||||||
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
|
response: { ok: true, status: 200 },
|
||||||
|
data: { items: [], total: 0 }
|
||||||
|
});
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
await load({
|
||||||
|
url: makeUrl({ q: 'Urlaub', from: '1920-01-01', to: '1950-12-31' }),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
'/api/documents/search',
|
||||||
|
expect.objectContaining({
|
||||||
|
params: expect.objectContaining({
|
||||||
|
query: expect.objectContaining({ q: 'Urlaub', from: '1920-01-01', to: '1950-12-31' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes senderId and receiverId to the search API', async () => {
|
||||||
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
|
response: { ok: true, status: 200 },
|
||||||
|
data: { items: [], total: 0 }
|
||||||
|
});
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
await load({
|
||||||
|
url: makeUrl({ senderId: 'p-1', receiverId: 'p-2' }),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
'/api/documents/search',
|
||||||
|
expect.objectContaining({
|
||||||
|
params: expect.objectContaining({
|
||||||
|
query: expect.objectContaining({ senderId: 'p-1', receiverId: 'p-2' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes sort, dir, tagQ to the search API', async () => {
|
||||||
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
|
response: { ok: true, status: 200 },
|
||||||
|
data: { items: [], total: 0 }
|
||||||
|
});
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
await load({
|
||||||
|
url: makeUrl({ sort: 'TITLE', dir: 'asc', tagQ: 'fam' }),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
'/api/documents/search',
|
||||||
|
expect.objectContaining({
|
||||||
|
params: expect.objectContaining({
|
||||||
|
query: expect.objectContaining({ sort: 'TITLE', dir: 'asc', tagQ: 'fam' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns items and total from the search result', async () => {
|
||||||
|
const item = {
|
||||||
|
document: { id: 'd1' },
|
||||||
|
matchData: {},
|
||||||
|
completionPercentage: 0,
|
||||||
|
contributors: []
|
||||||
|
};
|
||||||
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
|
response: { ok: true, status: 200 },
|
||||||
|
data: { items: [item], total: 42 }
|
||||||
|
});
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
const result = await load({
|
||||||
|
url: makeUrl({ q: 'test' }),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.total).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns filter values in the result for pre-filling the UI', async () => {
|
||||||
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
|
response: { ok: true, status: 200 },
|
||||||
|
data: { items: [], total: 0 }
|
||||||
|
});
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
const result = await load({
|
||||||
|
url: makeUrl({ q: 'Urlaub', from: '1920-01-01', sort: 'TITLE', dir: 'asc' }),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.q).toBe('Urlaub');
|
||||||
|
expect(result.from).toBe('1920-01-01');
|
||||||
|
expect(result.sort).toBe('TITLE');
|
||||||
|
expect(result.dir).toBe('asc');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 401 redirect ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('documents page load — auth redirect', () => {
|
||||||
|
it('redirects to /login when search API returns 401', async () => {
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({
|
||||||
|
GET: vi.fn().mockResolvedValue({ response: { ok: false, status: 401 }, data: null })
|
||||||
|
} as ReturnType<typeof createApiClient>);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch })
|
||||||
|
).rejects.toMatchObject({ location: '/login' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── network error fallback ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('documents page load — network error fallback', () => {
|
||||||
|
it('returns error string instead of throwing when API call throws', async () => {
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({
|
||||||
|
GET: vi.fn().mockRejectedValue(new Error('Network failure'))
|
||||||
|
} as ReturnType<typeof createApiClient>);
|
||||||
|
|
||||||
|
const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch });
|
||||||
|
|
||||||
|
expect(result.error).toBeTruthy();
|
||||||
|
expect(result.items).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
91
frontend/src/routes/documents/+page.server.ts
Normal file
91
frontend/src/routes/documents/+page.server.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type DocumentSearchItem = components['schemas']['DocumentSearchItem'];
|
||||||
|
|
||||||
|
const VALID_SORTS = ['DATE', 'TITLE', 'SENDER', 'RECEIVER', 'UPLOAD_DATE', 'RELEVANCE'] as const;
|
||||||
|
type ValidSort = (typeof VALID_SORTS)[number];
|
||||||
|
const VALID_DIRS = ['asc', 'desc'] as const;
|
||||||
|
type ValidDir = (typeof VALID_DIRS)[number];
|
||||||
|
|
||||||
|
export async function load({ url, fetch }) {
|
||||||
|
const q = url.searchParams.get('q') || '';
|
||||||
|
const from = url.searchParams.get('from') || '';
|
||||||
|
const to = url.searchParams.get('to') || '';
|
||||||
|
const senderId = url.searchParams.get('senderId') || '';
|
||||||
|
const receiverId = url.searchParams.get('receiverId') || '';
|
||||||
|
const tags = url.searchParams.getAll('tag');
|
||||||
|
const rawSort = url.searchParams.get('sort') ?? 'DATE';
|
||||||
|
const sort: ValidSort = (VALID_SORTS as readonly string[]).includes(rawSort)
|
||||||
|
? (rawSort as ValidSort)
|
||||||
|
: 'DATE';
|
||||||
|
const rawDir = url.searchParams.get('dir') ?? 'desc';
|
||||||
|
const dir: ValidDir = (VALID_DIRS as readonly string[]).includes(rawDir)
|
||||||
|
? (rawDir as ValidDir)
|
||||||
|
: 'desc';
|
||||||
|
const tagQ = url.searchParams.get('tagQ') || '';
|
||||||
|
const tagOp = url.searchParams.get('tagOp') === 'OR' ? 'OR' : 'AND';
|
||||||
|
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.GET('/api/documents/search', {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
q: q || undefined,
|
||||||
|
from: from || undefined,
|
||||||
|
to: to || undefined,
|
||||||
|
senderId: senderId || undefined,
|
||||||
|
receiverId: receiverId || undefined,
|
||||||
|
tag: tags.length ? tags : undefined,
|
||||||
|
tagQ: tagQ && !tags.length ? tagQ : undefined,
|
||||||
|
tagOp: tagOp === 'OR' ? 'OR' : undefined,
|
||||||
|
sort,
|
||||||
|
dir: dir || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response.status === 401) {
|
||||||
|
throw redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: DocumentSearchItem[] = (result.data?.items ?? []) as DocumentSearchItem[];
|
||||||
|
const total: number = result.data?.total ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
q,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
senderId,
|
||||||
|
receiverId,
|
||||||
|
tags,
|
||||||
|
sort,
|
||||||
|
dir,
|
||||||
|
tagQ,
|
||||||
|
tagOp,
|
||||||
|
error: null as string | null
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as { status?: number }).status) throw e;
|
||||||
|
console.error('Error loading documents:', e);
|
||||||
|
return {
|
||||||
|
items: [] as DocumentSearchItem[],
|
||||||
|
total: 0,
|
||||||
|
q,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
senderId,
|
||||||
|
receiverId,
|
||||||
|
tags,
|
||||||
|
sort,
|
||||||
|
dir,
|
||||||
|
tagQ,
|
||||||
|
tagOp,
|
||||||
|
error: 'Daten konnten nicht geladen werden.' as string | null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user