Files
LaunchPad/apps/backend/src/db/repositories/services.ts

197 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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).
* Für automatische Scan-Ergebnisse siehe upsertServiceFromScan() unten die
* NIEMALS displayName, category, favorite, order, alias oder icon überschreibt.
*/
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;
}
export interface ServiceScanInput {
deviceId: string;
hostname: string;
url: string;
https: boolean;
port: number;
favicon?: string | null;
description?: string | null;
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird. */
suggestedDisplayName: string;
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird. */
suggestedCategory?: string | null;
}
export interface ScanUpsertResult {
service: Service;
created: boolean;
}
function findByDeviceAndPort(deviceId: string, port: number): Service | null {
return listServicesByDevice(deviceId).find((s) => s.port === port) ?? null;
}
/**
* Legt einen per Scan gefundenen Dienst an oder aktualisiert die scan-eigenen
* Felder eines bereits bekannten Dienstes (abgeglichen über deviceId + port).
*
* displayName, category, favorite, order, alias und icon werden bei einem
* bestehenden Dienst NIEMALS verändert nur beim erstmaligen Anlegen dienen
* suggestedDisplayName/suggestedCategory als sinnvoller Startwert.
*/
export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult {
const existing = findByDeviceAndPort(input.deviceId, input.port);
const timestamp = nowIso();
if (existing) {
db.update(services)
.set({
hostname: input.hostname,
url: input.url,
https: input.https,
favicon: input.favicon ?? existing.favicon,
description: input.description ?? existing.description,
updatedAt: timestamp,
})
.where(eq(services.id, existing.id))
.run();
return { service: getService(existing.id)!, created: false };
}
const id = randomUUID();
db.insert(services)
.values({
id,
deviceId: input.deviceId,
displayName: input.suggestedDisplayName,
category: input.suggestedCategory ?? null,
favorite: false,
order: 0,
alias: "[]",
icon: null,
hostname: input.hostname,
url: input.url,
https: input.https,
port: input.port,
favicon: input.favicon ?? null,
description: input.description ?? null,
createdAt: timestamp,
updatedAt: timestamp,
})
.run();
return { service: getService(id)!, created: true };
}