generated from Dicken/dickendock
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
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 };
|
||
}
|