generated from Dicken/dickendock
Commit 2: REST-API für Geräte und Dienste (Zod-Validierung, Drizzle-Repositories)
This commit is contained in:
22
README.md
22
README.md
@@ -58,17 +58,35 @@ Dieser erste Commit liefert ein lauffähiges Grundgerüst:
|
|||||||
- ✅ Tastaturkürzel `/` und `Strg+K` zum Fokussieren der Suche
|
- ✅ Tastaturkürzel `/` und `Strg+K` zum Fokussieren der Suche
|
||||||
- ✅ Docker-Compose-Setup: `docker compose up -d --build` startet Frontend + Backend
|
- ✅ Docker-Compose-Setup: `docker compose up -d --build` startet Frontend + Backend
|
||||||
- ✅ Persistentes Docker-Volume für die SQLite-Datenbank
|
- ✅ Persistentes Docker-Volume für die SQLite-Datenbank
|
||||||
|
- ✅ REST-API für Geräte & Dienste (`/api/devices`, `/api/services`), Zod-validiert,
|
||||||
|
mit Repository-Layer über Drizzle (siehe `apps/backend/src/db/repositories`)
|
||||||
|
|
||||||
Noch **nicht** enthalten (folgt in den nächsten Commits):
|
Noch **nicht** enthalten (folgt in den nächsten Commits):
|
||||||
|
|
||||||
- Scanner (FritzBox, DNS, HTTP/HTTPS, Portscan, Softwareerkennung)
|
- Scanner (FritzBox, DNS, HTTP/HTTPS, Portscan, Softwareerkennung)
|
||||||
- CRUD-API für Geräte/Dienste/Kategorien
|
|
||||||
- Fuzzy-Suche im Frontend gegen echte Daten (Ranking-Logik existiert bereits in
|
- Fuzzy-Suche im Frontend gegen echte Daten (Ranking-Logik existiert bereits in
|
||||||
`packages/shared`, ist aber noch nicht ans UI angebunden)
|
`packages/shared`, ist aber noch nicht ans UI angebunden)
|
||||||
- Adminbereich, Plugin-System, PWA-Manifest, TanStack Router/Query, shadcn/ui, Zod, RHF
|
- Adminbereich, Plugin-System, PWA-Manifest, TanStack Router/Query, shadcn/ui, RHF
|
||||||
|
|
||||||
Siehe [`docs/ROADMAP.md`](./docs/ROADMAP.md) für die geplante Reihenfolge.
|
Siehe [`docs/ROADMAP.md`](./docs/ROADMAP.md) für die geplante Reihenfolge.
|
||||||
|
|
||||||
|
## API-Endpunkte (Stand Commit 2)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/health
|
||||||
|
GET /api/devices Liste aller Geräte inkl. ihrer Dienste
|
||||||
|
GET /api/devices/:id
|
||||||
|
POST /api/devices
|
||||||
|
PATCH /api/devices/:id
|
||||||
|
DELETE /api/devices/:id (löscht zugehörige Dienste per Cascade)
|
||||||
|
|
||||||
|
GET /api/services optional ?deviceId=&category=&favorite=true
|
||||||
|
GET /api/services/:id
|
||||||
|
POST /api/services erfordert existierende deviceId
|
||||||
|
PATCH /api/services/:id
|
||||||
|
DELETE /api/services/:id
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment auf dem Server (xlc-launchpad)
|
## Deployment auf dem Server (xlc-launchpad)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
86
apps/backend/src/db/repositories/devices.ts
Normal file
86
apps/backend/src/db/repositories/devices.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Device, DeviceCreateInput, DeviceUpdateInput } from "@launchpad/shared";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { devices } from "../schema.js";
|
||||||
|
|
||||||
|
function nowIso(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRow(row: typeof devices.$inferSelect): Device {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
hostname: row.hostname,
|
||||||
|
ip: row.ip,
|
||||||
|
mac: row.mac,
|
||||||
|
manufacturer: row.manufacturer,
|
||||||
|
model: row.model,
|
||||||
|
online: row.online,
|
||||||
|
source: row.source as Device["source"],
|
||||||
|
lastScan: row.lastScan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDevices(): Device[] {
|
||||||
|
return db.select().from(devices).all().map(mapRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDevice(id: string): Device | null {
|
||||||
|
const row = db.select().from(devices).where(eq(devices.id, id)).get();
|
||||||
|
return row ? mapRow(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDevice(input: DeviceCreateInput): Device {
|
||||||
|
const id = randomUUID();
|
||||||
|
const timestamp = nowIso();
|
||||||
|
|
||||||
|
db.insert(devices)
|
||||||
|
.values({
|
||||||
|
id,
|
||||||
|
hostname: input.hostname,
|
||||||
|
ip: input.ip,
|
||||||
|
mac: input.mac ?? null,
|
||||||
|
manufacturer: input.manufacturer ?? null,
|
||||||
|
model: input.model ?? null,
|
||||||
|
online: input.online ?? false,
|
||||||
|
source: input.source ?? "manual",
|
||||||
|
lastScan: null,
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return getDevice(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert ein Gerät anhand von Benutzereingaben (z. B. über die Admin-UI).
|
||||||
|
* Ein separater Pfad für automatische Scan-Ergebnisse folgt in einem späteren
|
||||||
|
* Commit (siehe docs/ROADMAP.md, Commit 5 – Scanner).
|
||||||
|
*/
|
||||||
|
export function updateDevice(id: string, input: DeviceUpdateInput): Device | null {
|
||||||
|
const existing = getDevice(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
db.update(devices)
|
||||||
|
.set({
|
||||||
|
...(input.hostname !== undefined && { hostname: input.hostname }),
|
||||||
|
...(input.ip !== undefined && { ip: input.ip }),
|
||||||
|
...(input.mac !== undefined && { mac: input.mac }),
|
||||||
|
...(input.manufacturer !== undefined && { manufacturer: input.manufacturer }),
|
||||||
|
...(input.model !== undefined && { model: input.model }),
|
||||||
|
...(input.online !== undefined && { online: input.online }),
|
||||||
|
...(input.source !== undefined && { source: input.source }),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
})
|
||||||
|
.where(eq(devices.id, id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return getDevice(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDevice(id: string): boolean {
|
||||||
|
const result = db.delete(devices).where(eq(devices.id, id)).run();
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
122
apps/backend/src/db/repositories/services.ts
Normal file
122
apps/backend/src/db/repositories/services.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Service, ServiceCreateInput, ServiceUpdateInput } from "@launchpad/shared";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { services } from "../schema.js";
|
||||||
|
|
||||||
|
function nowIso(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRow(row: typeof services.$inferSelect): Service {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
deviceId: row.deviceId,
|
||||||
|
displayName: row.displayName,
|
||||||
|
hostname: row.hostname,
|
||||||
|
url: row.url,
|
||||||
|
https: row.https,
|
||||||
|
port: row.port,
|
||||||
|
category: row.category,
|
||||||
|
icon: row.icon,
|
||||||
|
favicon: row.favicon,
|
||||||
|
description: row.description,
|
||||||
|
favorite: row.favorite,
|
||||||
|
alias: JSON.parse(row.alias) as string[],
|
||||||
|
order: row.order,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceFilter {
|
||||||
|
deviceId?: string;
|
||||||
|
category?: string;
|
||||||
|
favorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listServices(filter: ServiceFilter = {}): Service[] {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(services)
|
||||||
|
.all()
|
||||||
|
.map(mapRow)
|
||||||
|
.filter((service) => {
|
||||||
|
if (filter.deviceId && service.deviceId !== filter.deviceId) return false;
|
||||||
|
if (filter.category && service.category !== filter.category) return false;
|
||||||
|
if (filter.favorite !== undefined && service.favorite !== filter.favorite) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listServicesByDevice(deviceId: string): Service[] {
|
||||||
|
return listServices({ deviceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getService(id: string): Service | null {
|
||||||
|
const row = db.select().from(services).where(eq(services.id, id)).get();
|
||||||
|
return row ? mapRow(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createService(input: ServiceCreateInput): Service {
|
||||||
|
const id = randomUUID();
|
||||||
|
const timestamp = nowIso();
|
||||||
|
|
||||||
|
db.insert(services)
|
||||||
|
.values({
|
||||||
|
id,
|
||||||
|
deviceId: input.deviceId,
|
||||||
|
displayName: input.displayName,
|
||||||
|
hostname: input.hostname,
|
||||||
|
url: input.url,
|
||||||
|
https: input.https ?? false,
|
||||||
|
port: input.port,
|
||||||
|
category: input.category ?? null,
|
||||||
|
icon: input.icon ?? null,
|
||||||
|
favicon: input.favicon ?? null,
|
||||||
|
description: input.description ?? null,
|
||||||
|
favorite: input.favorite ?? false,
|
||||||
|
alias: JSON.stringify(input.alias ?? []),
|
||||||
|
order: input.order ?? 0,
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return getService(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert einen Dienst anhand von Benutzereingaben (z. B. über die Admin-UI).
|
||||||
|
* Ein separater Pfad für automatische Scan-Ergebnisse folgt in einem späteren
|
||||||
|
* Commit (siehe docs/ROADMAP.md, Commit 5 – Scanner): Scans dürfen displayName,
|
||||||
|
* category, favorite, order, alias und icon niemals überschreiben.
|
||||||
|
*/
|
||||||
|
export function updateService(id: string, input: ServiceUpdateInput): Service | null {
|
||||||
|
const existing = getService(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
db.update(services)
|
||||||
|
.set({
|
||||||
|
...(input.displayName !== undefined && { displayName: input.displayName }),
|
||||||
|
...(input.hostname !== undefined && { hostname: input.hostname }),
|
||||||
|
...(input.url !== undefined && { url: input.url }),
|
||||||
|
...(input.https !== undefined && { https: input.https }),
|
||||||
|
...(input.port !== undefined && { port: input.port }),
|
||||||
|
...(input.category !== undefined && { category: input.category }),
|
||||||
|
...(input.icon !== undefined && { icon: input.icon }),
|
||||||
|
...(input.favicon !== undefined && { favicon: input.favicon }),
|
||||||
|
...(input.description !== undefined && { description: input.description }),
|
||||||
|
...(input.favorite !== undefined && { favorite: input.favorite }),
|
||||||
|
...(input.alias !== undefined && { alias: JSON.stringify(input.alias) }),
|
||||||
|
...(input.order !== undefined && { order: input.order }),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
})
|
||||||
|
.where(eq(services.id, id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return getService(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteService(id: string): boolean {
|
||||||
|
const result = db.delete(services).where(eq(services.id, id)).run();
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import Fastify from "fastify";
|
|||||||
import cors from "@fastify/cors";
|
import cors from "@fastify/cors";
|
||||||
import { ensureSchema } from "./db/client.js";
|
import { ensureSchema } from "./db/client.js";
|
||||||
import { healthRoutes } from "./routes/health.js";
|
import { healthRoutes } from "./routes/health.js";
|
||||||
|
import { deviceRoutes } from "./routes/devices.js";
|
||||||
|
import { serviceRoutes } from "./routes/services.js";
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 3001);
|
const PORT = Number(process.env.PORT ?? 3001);
|
||||||
const HOST = process.env.HOST ?? "0.0.0.0";
|
const HOST = process.env.HOST ?? "0.0.0.0";
|
||||||
@@ -24,6 +26,8 @@ async function main() {
|
|||||||
ensureSchema();
|
ensureSchema();
|
||||||
|
|
||||||
await app.register(healthRoutes);
|
await app.register(healthRoutes);
|
||||||
|
await app.register(deviceRoutes);
|
||||||
|
await app.register(serviceRoutes);
|
||||||
|
|
||||||
app.get("/", async () => {
|
app.get("/", async () => {
|
||||||
return { name: "LaunchPad API", status: "running" };
|
return { name: "LaunchPad API", status: "running" };
|
||||||
|
|||||||
53
apps/backend/src/routes/devices.ts
Normal file
53
apps/backend/src/routes/devices.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { DeviceCreateSchema, DeviceUpdateSchema } from "@launchpad/shared";
|
||||||
|
import * as deviceRepo from "../db/repositories/devices.js";
|
||||||
|
import * as serviceRepo from "../db/repositories/services.js";
|
||||||
|
|
||||||
|
export async function deviceRoutes(app: FastifyInstance): Promise<void> {
|
||||||
|
app.get("/api/devices", async () => {
|
||||||
|
return deviceRepo.listDevices().map((device) => ({
|
||||||
|
...device,
|
||||||
|
services: serviceRepo.listServicesByDevice(device.id),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/devices/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const device = deviceRepo.getDevice(id);
|
||||||
|
if (!device) {
|
||||||
|
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||||
|
}
|
||||||
|
return { ...device, services: serviceRepo.listServicesByDevice(id) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/devices", async (request, reply) => {
|
||||||
|
const parsed = DeviceCreateSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||||
|
}
|
||||||
|
const device = deviceRepo.createDevice(parsed.data);
|
||||||
|
return reply.code(201).send(device);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/api/devices/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const parsed = DeviceUpdateSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||||
|
}
|
||||||
|
const device = deviceRepo.updateDevice(id, parsed.data);
|
||||||
|
if (!device) {
|
||||||
|
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||||
|
}
|
||||||
|
return device;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/devices/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const deleted = deviceRepo.deleteDevice(id);
|
||||||
|
if (!deleted) {
|
||||||
|
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||||
|
}
|
||||||
|
return reply.code(204).send();
|
||||||
|
});
|
||||||
|
}
|
||||||
67
apps/backend/src/routes/services.ts
Normal file
67
apps/backend/src/routes/services.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { ServiceCreateSchema, ServiceUpdateSchema } from "@launchpad/shared";
|
||||||
|
import * as deviceRepo from "../db/repositories/devices.js";
|
||||||
|
import * as serviceRepo from "../db/repositories/services.js";
|
||||||
|
|
||||||
|
interface ServiceListQuery {
|
||||||
|
deviceId?: string;
|
||||||
|
category?: string;
|
||||||
|
favorite?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serviceRoutes(app: FastifyInstance): Promise<void> {
|
||||||
|
app.get("/api/services", async (request) => {
|
||||||
|
const query = request.query as ServiceListQuery;
|
||||||
|
return serviceRepo.listServices({
|
||||||
|
deviceId: query.deviceId,
|
||||||
|
category: query.category,
|
||||||
|
favorite: query.favorite === undefined ? undefined : query.favorite === "true",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/services/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const service = serviceRepo.getService(id);
|
||||||
|
if (!service) {
|
||||||
|
return reply.code(404).send({ error: "Dienst nicht gefunden" });
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/services", async (request, reply) => {
|
||||||
|
const parsed = ServiceCreateSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = deviceRepo.getDevice(parsed.data.deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return reply.code(400).send({ error: `Gerät ${parsed.data.deviceId} existiert nicht` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = serviceRepo.createService(parsed.data);
|
||||||
|
return reply.code(201).send(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/api/services/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const parsed = ServiceUpdateSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||||
|
}
|
||||||
|
const service = serviceRepo.updateService(id, parsed.data);
|
||||||
|
if (!service) {
|
||||||
|
return reply.code(404).send({ error: "Dienst nicht gefunden" });
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/services/:id", async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const deleted = serviceRepo.deleteService(id);
|
||||||
|
if (!deleted) {
|
||||||
|
return reply.code(404).send({ error: "Dienst nicht gefunden" });
|
||||||
|
}
|
||||||
|
return reply.code(204).send();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
Geplante Reihenfolge der nächsten Commits, aufbauend auf dem lauffähigen
|
Geplante Reihenfolge der nächsten Commits, aufbauend auf dem lauffähigen
|
||||||
Grundgerüst aus Commit 1.
|
Grundgerüst aus Commit 1.
|
||||||
|
|
||||||
## Commit 2 — Geräte- & Dienste-API
|
## ✅ Commit 2 — Geräte- & Dienste-API (erledigt)
|
||||||
|
|
||||||
- REST-Endpunkte (Fastify) für `GET/POST/PATCH/DELETE` auf `devices` und `services`
|
- REST-Endpunkte (Fastify) für `GET/POST/PATCH/DELETE` auf `devices` und `services`
|
||||||
- Zod-Validierung der Request-Bodies
|
- Zod-Validierung der Request-Bodies (`packages/shared/src/schemas.ts`)
|
||||||
- Repository-Layer über Drizzle statt Rohschema-Zugriff
|
- Repository-Layer über Drizzle (`apps/backend/src/db/repositories`)
|
||||||
|
|
||||||
## Commit 3 — Suche im Frontend
|
## Commit 3 — Suche im Frontend
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
* als auch vom Frontend (apps/frontend) verwendet werden.
|
* als auch vom Frontend (apps/frontend) verwendet werden.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./schemas.js";
|
||||||
|
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: string;
|
id: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|||||||
48
packages/shared/src/schemas.ts
Normal file
48
packages/shared/src/schemas.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const deviceSourceValues = [
|
||||||
|
"fritzbox",
|
||||||
|
"dns",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"portscan",
|
||||||
|
"manual",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const DeviceSourceSchema = z.enum(deviceSourceValues);
|
||||||
|
|
||||||
|
export const DeviceCreateSchema = z.object({
|
||||||
|
hostname: z.string().min(1, "hostname darf nicht leer sein"),
|
||||||
|
ip: z.string().min(1, "ip darf nicht leer sein"),
|
||||||
|
mac: z.string().optional(),
|
||||||
|
manufacturer: z.string().optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
|
online: z.boolean().optional(),
|
||||||
|
source: DeviceSourceSchema.optional(),
|
||||||
|
});
|
||||||
|
export type DeviceCreateInput = z.infer<typeof DeviceCreateSchema>;
|
||||||
|
|
||||||
|
export const DeviceUpdateSchema = DeviceCreateSchema.partial();
|
||||||
|
export type DeviceUpdateInput = z.infer<typeof DeviceUpdateSchema>;
|
||||||
|
|
||||||
|
export const ServiceCreateSchema = z.object({
|
||||||
|
deviceId: z.string().min(1, "deviceId darf nicht leer sein"),
|
||||||
|
displayName: z.string().min(1, "displayName darf nicht leer sein"),
|
||||||
|
hostname: z.string().min(1, "hostname darf nicht leer sein"),
|
||||||
|
url: z.string().url("url muss eine gültige URL sein"),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
port: z.number().int().min(1).max(65535),
|
||||||
|
category: z.string().optional(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
favicon: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
favorite: z.boolean().optional(),
|
||||||
|
alias: z.array(z.string()).optional(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
});
|
||||||
|
export type ServiceCreateInput = z.infer<typeof ServiceCreateSchema>;
|
||||||
|
|
||||||
|
export const ServiceUpdateSchema = ServiceCreateSchema.omit({
|
||||||
|
deviceId: true,
|
||||||
|
}).partial();
|
||||||
|
export type ServiceUpdateInput = z.infer<typeof ServiceUpdateSchema>;
|
||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -90,6 +90,10 @@ importers:
|
|||||||
version: 5.4.21(@types/node@20.19.43)
|
version: 5.4.21(@types/node@20.19.43)
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.8
|
||||||
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.4
|
specifier: ^5.5.4
|
||||||
@@ -1878,6 +1882,9 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -3356,3 +3363,5 @@ snapshots:
|
|||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user