Added zod for validation

This commit is contained in:
Denis Urs Rudolph
2025-12-12 12:21:03 +01:00
parent 2b164d6525
commit 7f6db6d004
4 changed files with 160 additions and 36 deletions

96
lib/api.test.ts Normal file
View File

@@ -0,0 +1,96 @@
import { expect, test, describe } from "bun:test";
import { VehicleSchema, VehicleGroupSchema, FirmwareUpdateSchema, DeploymentSchema } from "./api";
describe("Schema Validation", () => {
describe("VehicleSchema", () => {
test("should validate valid vehicle object", () => {
const validVehicle = {
vin: "TEST12345",
status: "Online",
currentVersion: "1.0.0",
lastHeartbeat: "2023-01-01T12:00:00Z",
groupId: 1,
group: {
id: 1,
name: "Test Group"
}
};
const result = VehicleSchema.safeParse(validVehicle);
expect(result.success).toBe(true);
});
test("should fail on missing required fields", () => {
const invalidVehicle = {
vin: "TEST12345",
// status missing
currentVersion: "1.0.0"
};
const result = VehicleSchema.safeParse(invalidVehicle);
expect(result.success).toBe(false);
});
test("should allow null/optional fields", () => {
const vehicleWithoutGroup = {
vin: "TEST_NO_GROUP",
status: "Offline",
currentVersion: "2.0",
lastHeartbeat: "2023-01-01T10:00:00Z",
groupId: null,
group: null
};
const result = VehicleSchema.safeParse(vehicleWithoutGroup);
expect(result.success).toBe(true);
});
});
describe("VehicleGroupSchema", () => {
test("should validate valid group", () => {
const validGroup = {
id: 10,
name: "Production",
description: "Main fleet",
vehicles: []
};
const result = VehicleGroupSchema.safeParse(validGroup);
expect(result.success).toBe(true);
});
test("should fail with invalid types", () => {
const invalidGroup = {
id: "should be number",
name: "Test"
};
const result = VehicleGroupSchema.safeParse(invalidGroup);
expect(result.success).toBe(false);
});
});
describe("FirmwareUpdateSchema", () => {
test("should validate update object", () => {
const update = {
id: 5,
version: "1.2.3",
uploadedAt: "2023-10-10",
description: "Security Patch"
};
expect(FirmwareUpdateSchema.safeParse(update).success).toBe(true);
});
});
describe("DeploymentSchema", () => {
test("should validate deployment", () => {
const deployment = {
id: 1,
updateId: 5,
status: "Pending",
createdAt: "2023-10-11",
update: {
id: 5,
version: "1.2.3",
uploadedAt: "2023-10-10"
}
};
expect(DeploymentSchema.safeParse(deployment).success).toBe(true);
});
});
});

View File

@@ -1,54 +1,77 @@
import { z } from 'zod';
const API_URL = 'http://localhost:5000/api';
export interface Vehicle {
vin: string;
status: string;
currentVersion: string;
lastHeartbeat: string;
groupId?: number;
group?: VehicleGroup;
}
// --- Zod Schemas ---
export interface VehicleGroup {
id: number;
name: string;
description?: string;
vehicles?: Vehicle[];
}
// Base schemas to handle circular references if needed, though usually mapped by API
// defined separately to allow recursive type inference if we went down the z.lazy route,
// but for now we'll keep it simple as the API returns what it returns.
export interface FirmwareUpdate {
id: number;
version: string;
description?: string;
uploadedAt: string;
}
export const BaseVehicleGroupSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string().optional().nullable(),
});
export interface Deployment {
id: number;
updateId: number;
targetVin?: string;
targetGroupId?: number;
status: string;
createdAt: string;
update: FirmwareUpdate;
}
export const VehicleSchema = z.object({
vin: z.string(),
status: z.string(),
currentVersion: z.string(),
lastHeartbeat: z.string(),
groupId: z.number().optional().nullable(),
group: BaseVehicleGroupSchema.optional().nullable(),
});
export const VehicleGroupSchema = BaseVehicleGroupSchema.extend({
vehicles: z.array(VehicleSchema).optional().nullable(),
});
export const FirmwareUpdateSchema = z.object({
id: z.number(),
version: z.string(),
description: z.string().optional().nullable(),
uploadedAt: z.string(),
});
export const DeploymentSchema = z.object({
id: z.number(),
updateId: z.number(),
targetVin: z.string().optional().nullable(),
targetGroupId: z.number().optional().nullable(),
status: z.string(),
createdAt: z.string(),
update: FirmwareUpdateSchema,
});
// --- Types Inferred from Schemas ---
export type Vehicle = z.infer<typeof VehicleSchema>;
export type VehicleGroup = z.infer<typeof VehicleGroupSchema>;
export type FirmwareUpdate = z.infer<typeof FirmwareUpdateSchema>;
export type Deployment = z.infer<typeof DeploymentSchema>;
// --- API Functions with Validation ---
export async function fetchVehicles(): Promise<Vehicle[]> {
const res = await fetch(`${API_URL}/admin/vehicles`);
if (!res.ok) throw new Error('Failed to fetch vehicles');
return res.json();
const json = await res.json();
return z.array(VehicleSchema).parse(json);
}
export async function fetchGroups(): Promise<VehicleGroup[]> {
const res = await fetch(`${API_URL}/admin/groups`);
if (!res.ok) throw new Error('Failed to fetch groups');
return res.json();
const json = await res.json();
return z.array(VehicleGroupSchema).parse(json);
}
export async function getUpdates(): Promise<FirmwareUpdate[]> {
const res = await fetch(`${API_URL}/admin/updates`);
if (!res.ok) throw new Error('Failed to fetch updates');
return res.json();
const json = await res.json();
return z.array(FirmwareUpdateSchema).parse(json);
}
export async function createGroup(name: string, description?: string): Promise<VehicleGroup> {
@@ -58,7 +81,8 @@ export async function createGroup(name: string, description?: string): Promise<V
body: JSON.stringify({ name, description }),
});
if (!res.ok) throw new Error('Failed to create group');
return res.json();
const json = await res.json();
return VehicleGroupSchema.parse(json);
}
export async function updateGroup(id: number, name: string, description?: string): Promise<VehicleGroup> {
@@ -68,7 +92,8 @@ export async function updateGroup(id: number, name: string, description?: string
body: JSON.stringify({ name, description }),
});
if (!res.ok) throw new Error('Failed to update group');
return res.json();
const json = await res.json();
return VehicleGroupSchema.parse(json);
}
export async function assignVehicleToGroup(groupId: number, vin: string): Promise<void> {
@@ -83,7 +108,8 @@ export async function assignVehicleToGroup(groupId: number, vin: string): Promis
export async function fetchDeployments(): Promise<Deployment[]> {
const res = await fetch(`${API_URL}/admin/deployments`);
if (!res.ok) throw new Error('Failed to fetch deployments');
return res.json();
const json = await res.json();
return z.array(DeploymentSchema).parse(json);
}
export async function uploadUpdate(formData: FormData): Promise<void> {