80 lines
2.1 KiB
TypeScript
80 lines
2.1 KiB
TypeScript
|
|
import { z } from 'zod';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Runtime mirrors of the TypeScript interfaces in `src/types/index.ts`.
|
||
|
|
*
|
||
|
|
* A TS interface is a compile-time-only promise: nothing stops the Django
|
||
|
|
* side from renaming a field, changing its nullability, or dropping a facet,
|
||
|
|
* and the frontend would only find out at runtime, in production, as a blank
|
||
|
|
* screen or a silently-wrong render. These schemas are validated against the
|
||
|
|
* real API response in `search-response.contract.test.ts`, so that class of
|
||
|
|
* drift fails a test instead.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export const FacetBucketSchema = z.object({
|
||
|
|
key: z.string(),
|
||
|
|
count: z.number().int().nonnegative(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const FACET_NAMES = [
|
||
|
|
'skills',
|
||
|
|
'job_title_role',
|
||
|
|
'seniority',
|
||
|
|
'country',
|
||
|
|
'industry',
|
||
|
|
'company',
|
||
|
|
] as const;
|
||
|
|
|
||
|
|
export const SearchHitSchema = z.object({
|
||
|
|
id: z.number(),
|
||
|
|
score: z.number(),
|
||
|
|
full_name: z.string(),
|
||
|
|
headline: z.string(),
|
||
|
|
job_title: z.string(),
|
||
|
|
job_title_role: z.string(),
|
||
|
|
seniority_levels: z.array(z.string()),
|
||
|
|
company: z.string(),
|
||
|
|
industry: z.string(),
|
||
|
|
location_name: z.string(),
|
||
|
|
location_country: z.string(),
|
||
|
|
years_experience: z.number().nullable(),
|
||
|
|
linkedin_url: z.string(),
|
||
|
|
skills: z.array(z.string()),
|
||
|
|
highlight: z.record(z.string(), z.array(z.string())),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const SearchResponseSchema = z.object({
|
||
|
|
count: z.number().int().nonnegative(),
|
||
|
|
page: z.number().int().positive(),
|
||
|
|
page_size: z.number().int().positive(),
|
||
|
|
total_pages: z.number().int().nonnegative(),
|
||
|
|
took_ms: z.number().int().nonnegative(),
|
||
|
|
backend: z.string(),
|
||
|
|
results: z.array(SearchHitSchema),
|
||
|
|
facets: z.record(z.enum(FACET_NAMES), z.array(FacetBucketSchema)),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const UserSchema = z.object({
|
||
|
|
id: z.number(),
|
||
|
|
email: z.string(),
|
||
|
|
full_name: z.string(),
|
||
|
|
role: z.enum(['admin', 'analyst', 'viewer']),
|
||
|
|
can_view_pii: z.boolean(),
|
||
|
|
date_joined: z.string(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const TokenPairSchema = z.object({
|
||
|
|
access: z.string(),
|
||
|
|
refresh: z.string(),
|
||
|
|
user: UserSchema,
|
||
|
|
});
|
||
|
|
|
||
|
|
export const ApiErrorSchema = z.object({
|
||
|
|
error: z.object({
|
||
|
|
code: z.string(),
|
||
|
|
message: z.string(),
|
||
|
|
details: z.unknown().optional(),
|
||
|
|
request_id: z.string(),
|
||
|
|
}),
|
||
|
|
});
|