179 lines
4.2 KiB
TypeScript
179 lines
4.2 KiB
TypeScript
import { z } from "zod"
|
|
import { AW_API_BASE, AW_API_TIMEOUT_MS } from "./constants"
|
|
|
|
// --- Zod Schemas ---
|
|
|
|
export const topicSchema = z.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
abgeordnetenwatch_url: z.string().optional(),
|
|
})
|
|
|
|
export const politicianSchema = z.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
party: z
|
|
.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
})
|
|
|
|
const pollTopicSchema = z.object({
|
|
id: z.number(),
|
|
label: z.string().optional(),
|
|
abgeordnetenwatch_url: z.string().optional(),
|
|
})
|
|
|
|
export const pollSchema = z.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
abgeordnetenwatch_url: z.string().optional(),
|
|
field_poll_date: z.string().nullable(),
|
|
field_accepted: z.boolean().nullable().optional(),
|
|
field_topics: z.array(pollTopicSchema),
|
|
})
|
|
|
|
export const candidacyMandateSchema = z.object({
|
|
id: z.number(),
|
|
})
|
|
|
|
const fractionMembershipSchema = z.object({
|
|
fraction: z.object({
|
|
label: z.string(),
|
|
}),
|
|
valid_until: z.string().nullable().optional(),
|
|
})
|
|
|
|
export const mandateWithPoliticianSchema = z.object({
|
|
id: z.number(),
|
|
politician: z.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
}),
|
|
party: z
|
|
.object({
|
|
id: z.number(),
|
|
label: z.string(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
fraction_membership: z.array(fractionMembershipSchema).optional(),
|
|
electoral_data: z
|
|
.object({
|
|
constituency: z
|
|
.object({
|
|
label: z.string(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
mandate_won: z.string().nullable().optional(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
})
|
|
|
|
export const voteSchema = z.object({
|
|
id: z.number(),
|
|
poll: z
|
|
.object({
|
|
id: z.number(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
})
|
|
|
|
// --- Types ---
|
|
|
|
export type Topic = z.infer<typeof topicSchema>
|
|
export type Politician = z.infer<typeof politicianSchema>
|
|
export type Poll = z.infer<typeof pollSchema>
|
|
export type CandidacyMandate = z.infer<typeof candidacyMandateSchema>
|
|
export type MandateWithPolitician = z.infer<typeof mandateWithPoliticianSchema>
|
|
export type Vote = z.infer<typeof voteSchema>
|
|
|
|
// --- Fetch helper ---
|
|
|
|
async function request<T>(path: string, params: Record<string, string>, schema: z.ZodType<T>): Promise<T[]> {
|
|
const url = new URL(`${AW_API_BASE}/${path}`)
|
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
|
|
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => controller.abort(), AW_API_TIMEOUT_MS)
|
|
let res: Response
|
|
try {
|
|
res = await fetch(url.toString(), {
|
|
signal: controller.signal,
|
|
headers: { Accept: "application/json" },
|
|
})
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => "")
|
|
throw new Error(`AW API ${res.status} for ${url}: ${body}`)
|
|
}
|
|
|
|
const json = (await res.json()) as { data: unknown[] }
|
|
return z.array(schema).parse(json.data)
|
|
}
|
|
|
|
// --- Public API ---
|
|
|
|
export function fetchTopics(): Promise<Topic[]> {
|
|
return request("topics", { range_end: "200" }, topicSchema)
|
|
}
|
|
|
|
export function searchPoliticians(query: string): Promise<Politician[]> {
|
|
return request("politicians", { "label[cn]": query, range_end: "50" }, politicianSchema)
|
|
}
|
|
|
|
export function fetchPolls(rangeEnd = 100): Promise<Poll[]> {
|
|
return request(
|
|
"polls",
|
|
{
|
|
range_end: String(rangeEnd),
|
|
sort_by: "field_poll_date",
|
|
sort_direction: "desc",
|
|
},
|
|
pollSchema,
|
|
)
|
|
}
|
|
|
|
export async function fetchPollsByIds(ids: number[]): Promise<Poll[]> {
|
|
if (ids.length === 0) return []
|
|
// AW API does not support id[in] on /polls — fetch individually and dedupe
|
|
const results = await Promise.all(ids.map((id) => request("polls", { id: String(id), range_end: "1" }, pollSchema)))
|
|
return results.flat()
|
|
}
|
|
|
|
export function fetchCandidacyMandates(politicianID: number): Promise<CandidacyMandate[]> {
|
|
return request(
|
|
"candidacies-mandates",
|
|
{
|
|
politician: String(politicianID),
|
|
range_end: "200",
|
|
},
|
|
candidacyMandateSchema,
|
|
)
|
|
}
|
|
|
|
export function fetchVotes(mandateID: number): Promise<Vote[]> {
|
|
return request("votes", { mandate: String(mandateID), range_end: "200" }, voteSchema)
|
|
}
|
|
|
|
export function fetchMandatesByParliamentPeriod(periodID: number): Promise<MandateWithPolitician[]> {
|
|
return request(
|
|
"candidacies-mandates",
|
|
{
|
|
parliament_period: String(periodID),
|
|
type: "mandate",
|
|
range_end: "800",
|
|
},
|
|
mandateWithPoliticianSchema,
|
|
)
|
|
}
|