Files
agw/src/shared/lib/aw-api.ts

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,
)
}