Added Entity Wiki

This commit is contained in:
2026-01-27 15:01:56 +01:00
parent 90560ecd2c
commit e58929d9a0
5 changed files with 460 additions and 64 deletions

View File

@@ -1,17 +1,37 @@
import { FastifyInstance } from "fastify"
import { and, eq, isNull, asc, desc } from "drizzle-orm"
import { wikiPages, authUsers } from "../../db/schema/"
import { and, eq, isNull, asc, inArray } from "drizzle-orm"
// WICHTIG: Hier müssen die Schemas der Entitäten importiert werden!
import {
wikiPages,
authUsers,
customers,
projects,
plants,
products,
inventoryitems,
// ... weitere Schemas hier importieren
} from "../../db/schema/"
// Types für Request Body & Query (statt Zod)
// Konfiguration: Welche Entitäten sollen im Wiki auftauchen?
const ENTITY_CONFIG: Record<string, { table: any, labelField: any, rootLabel: string, idField: 'id' | 'uuid' }> = {
'customers': { table: customers, labelField: customers.name, rootLabel: 'Kunden', idField: 'id' },
'projects': { table: projects, labelField: projects.name, rootLabel: 'Projekte', idField: 'id' },
'plants': { table: plants, labelField: plants.name, rootLabel: 'Objekte', idField: 'id' },
'products': { table: products, labelField: products.name, rootLabel: 'Artikel', idField: 'id' },
'inventoryitems': { table: inventoryitems, labelField: inventoryitems.name, rootLabel: 'Inventarartikel', idField: 'id' },
// Hier weitere hinzufügen (z.B. vehicles, products...)
}
// Types
interface WikiTreeQuery {
entityType?: string
entityId?: number // Kommt als string oder number an, je nach parser
entityId?: number
entityUuid?: string
}
interface WikiCreateBody {
title: string
parentId?: string // UUID
parentId?: string
isFolder?: boolean
entityType?: string
entityId?: number
@@ -20,42 +40,53 @@ interface WikiCreateBody {
interface WikiUpdateBody {
title?: string
content?: any // Das Tiptap JSON Object
content?: any
parentId?: string | null
sortOrder?: number
isFolder?: boolean
}
export default async function wikiRoutes (server: FastifyInstance) {
export default async function wikiRoutes(server: FastifyInstance) {
// ---------------------------------------------------------
// 1. GET /wiki/tree
// Lädt die Struktur für die Sidebar (OHNE Content -> Schnell)
// Lädt Struktur: Entweder gefiltert (Widget) oder Global (mit virtuellen Ordnern)
// ---------------------------------------------------------
server.get<{ Querystring: WikiTreeQuery }>("/wiki/tree", async (req, reply) => {
const user = req.user
const { entityType, entityId, entityUuid } = req.query
// Basis-Filter: Tenant Sicherheit
const filters = [eq(wikiPages.tenantId, user.tenant_id)]
// FALL A: WIDGET-ANSICHT (Spezifische Entität)
// Wenn wir spezifisch filtern, wollen wir nur die echten Seiten ohne virtuelle Ordner
if (entityType && (entityId || entityUuid)) {
const filters = [
eq(wikiPages.tenantId, user.tenant_id),
eq(wikiPages.entityType, entityType)
]
// Logik: Laden wir "Allgemein" oder "Entitäts-Spezifisch"?
if (entityType) {
// Spezifisch (z.B. Kunde)
filters.push(eq(wikiPages.entityType, entityType))
if (entityId) filters.push(eq(wikiPages.entityId, Number(entityId)))
else if (entityUuid) filters.push(eq(wikiPages.entityUuid, entityUuid))
if (entityId) {
filters.push(eq(wikiPages.entityId, Number(entityId)))
} else if (entityUuid) {
filters.push(eq(wikiPages.entityUuid, entityUuid))
}
} else {
// Allgemein (alles wo entityType NULL ist)
filters.push(isNull(wikiPages.entityType))
return server.db
.select({
id: wikiPages.id,
parentId: wikiPages.parentId,
title: wikiPages.title,
isFolder: wikiPages.isFolder,
sortOrder: wikiPages.sortOrder,
entityType: wikiPages.entityType,
updatedAt: wikiPages.updatedAt,
})
.from(wikiPages)
.where(and(...filters))
.orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title))
}
// Performance: Nur Metadaten laden, kein riesiges JSON-Content
const pages = await server.db
// FALL B: GLOBALE ANSICHT (Haupt-Wiki)
// Wir laden ALLES und bauen virtuelle Ordner für die Entitäten
// 1. Alle Wiki-Seiten des Tenants laden
const allPages = await server.db
.select({
id: wikiPages.id,
parentId: wikiPages.parentId,
@@ -63,24 +94,115 @@ export default async function wikiRoutes (server: FastifyInstance) {
isFolder: wikiPages.isFolder,
sortOrder: wikiPages.sortOrder,
entityType: wikiPages.entityType,
entityId: wikiPages.entityId, // Wichtig für Zuordnung
entityUuid: wikiPages.entityUuid, // Wichtig für Zuordnung
updatedAt: wikiPages.updatedAt,
})
.from(wikiPages)
.where(and(...filters))
.where(eq(wikiPages.tenantId, user.tenant_id))
.orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title))
return pages
// Trennen in Standard-Seiten und Entity-Seiten
const standardPages = allPages.filter(p => !p.entityType)
const entityPages = allPages.filter(p => p.entityType)
const virtualNodes: any[] = []
// 2. Virtuelle Ordner generieren
// Wir iterieren durch unsere Config (Kunden, Projekte...)
await Promise.all(Object.entries(ENTITY_CONFIG).map(async ([typeKey, config]) => {
// Haben wir überhaupt Notizen für diesen Typ?
const pagesForType = entityPages.filter(p => p.entityType === typeKey)
if (pagesForType.length === 0) return
// IDs sammeln, um Namen aus der DB zu holen
// Wir unterscheiden zwischen ID (int) und UUID
let entities: any[] = []
if (config.idField === 'id') {
const ids = [...new Set(pagesForType.map(p => p.entityId).filter((id): id is number => id !== null))]
if (ids.length > 0) {
//@ts-ignore - Drizzle Typisierung bei dynamischen Tables ist tricky
entities = await server.db.select({ id: config.table.id, label: config.labelField })
.from(config.table)
//@ts-ignore
.where(inArray(config.table.id, ids))
}
} else {
// Falls UUID genutzt wird (z.B. IoT Devices)
const uuids = [...new Set(pagesForType.map(p => p.entityUuid).filter((uuid): uuid is string => uuid !== null))]
if (uuids.length > 0) {
//@ts-ignore
entities = await server.db.select({ id: config.table.id, label: config.labelField })
.from(config.table)
//@ts-ignore
.where(inArray(config.table.id, uuids))
}
}
if (entities.length === 0) return
// 3. Virtuellen Root Ordner erstellen (z.B. "Kunden")
const rootId = `virtual-root-${typeKey}`
virtualNodes.push({
id: rootId,
parentId: null, // Ganz oben im Baum
title: config.rootLabel,
isFolder: true,
isVirtual: true, // Flag fürs Frontend (read-only Folder)
sortOrder: 1000 // Ganz unten anzeigen
})
// 4. Virtuelle Entity Ordner erstellen (z.B. "Müller GmbH")
entities.forEach(entity => {
const entityNodeId = `virtual-entity-${typeKey}-${entity.id}`
virtualNodes.push({
id: entityNodeId,
parentId: rootId,
title: entity.label || 'Unbekannt',
isFolder: true,
isVirtual: true,
sortOrder: 0
})
// 5. Die echten Notizen verschieben
// Wir suchen alle Notizen, die zu dieser Entity gehören
const myPages = pagesForType.filter(p =>
(config.idField === 'id' && p.entityId === entity.id) ||
(config.idField === 'uuid' && p.entityUuid === entity.id)
)
myPages.forEach(page => {
// Nur Root-Notizen der Entity verschieben.
// Sub-Pages bleiben wo sie sind (parentId zeigt ja schon auf die richtige Seite)
if (!page.parentId) {
// Wir modifizieren das Objekt für die Response (nicht in der DB!)
// Wir müssen es clonen, sonst ändern wir es für alle Referenzen
const pageClone = { ...page }
pageClone.parentId = entityNodeId
virtualNodes.push(pageClone)
} else {
// Sub-Pages einfach so hinzufügen
virtualNodes.push(page)
}
})
})
}))
// Ergebnis: Normale Seiten + Virtuelle Struktur
return [...standardPages, ...virtualNodes]
})
// ---------------------------------------------------------
// 2. GET /wiki/:id
// Lädt EINEN Eintrag komplett MIT Content (für den Editor)
// Lädt EINEN Eintrag komplett MIT Content
// ---------------------------------------------------------
server.get<{ Params: { id: string } }>("/wiki/:id", async (req, reply) => {
const user = req.user
const { id } = req.params
// Drizzle Query API nutzen für Relation (optional Author laden)
const page = await server.db.query.wikiPages.findFirst({
where: and(
eq(wikiPages.id, id),
@@ -88,18 +210,12 @@ export default async function wikiRoutes (server: FastifyInstance) {
),
with: {
author: {
columns: {
id: true,
// name: true, // Falls im authUsers schema vorhanden
}
columns: { id: true } // Name falls vorhanden
}
}
})
if (!page) {
return reply.code(404).send({ error: "Page not found" })
}
if (!page) return reply.code(404).send({ error: "Page not found" })
return page
})
@@ -111,10 +227,8 @@ export default async function wikiRoutes (server: FastifyInstance) {
const user = req.user
const body = req.body
// Simple Validierung
if (!body.title) return reply.code(400).send({ error: "Title required" })
// Split Logik für Polymorphie
const hasEntity = !!body.entityType
const [newPage] = await server.db
@@ -122,10 +236,8 @@ export default async function wikiRoutes (server: FastifyInstance) {
.values({
tenantId: user.tenant_id,
title: body.title,
parentId: body.parentId || null, // undefined abfangen
parentId: body.parentId || null,
isFolder: body.isFolder ?? false,
// Polymorphe Zuweisung
entityType: hasEntity ? body.entityType : null,
entityId: hasEntity && body.entityId ? body.entityId : null,
entityUuid: hasEntity && body.entityUuid ? body.entityUuid : null,
@@ -141,7 +253,7 @@ export default async function wikiRoutes (server: FastifyInstance) {
// ---------------------------------------------------------
// 4. PATCH /wiki/:id
// Universal-Update (Inhalt, Titel, Verschieben, Sortieren)
// Universal-Update
// ---------------------------------------------------------
server.patch<{ Params: { id: string }; Body: WikiUpdateBody }>(
"/wiki/:id",
@@ -150,21 +262,19 @@ export default async function wikiRoutes (server: FastifyInstance) {
const { id } = req.params
const body = req.body
// 1. Prüfen ob Eintrag existiert & Tenant gehört
const existing = await server.db.query.wikiPages.findFirst({
where: and(eq(wikiPages.id, id), eq(wikiPages.tenantId, user.tenant_id)),
columns: { id: true }
})
if(!existing) return reply.code(404).send({ error: "Not found" })
if (!existing) return reply.code(404).send({ error: "Not found" })
// 2. Update durchführen
const [updatedPage] = await server.db
.update(wikiPages)
.set({
title: body.title, // update title if defined
content: body.content, // update content if defined
parentId: body.parentId, // update parent (move) if defined
title: body.title,
content: body.content,
parentId: body.parentId,
sortOrder: body.sortOrder,
isFolder: body.isFolder,
updatedAt: new Date(),
@@ -180,7 +290,7 @@ export default async function wikiRoutes (server: FastifyInstance) {
// ---------------------------------------------------------
// 5. DELETE /wiki/:id
// Löscht Eintrag (DB Cascade erledigt die Kinder)
// Löscht Eintrag
// ---------------------------------------------------------
server.delete<{ Params: { id: string } }>("/wiki/:id", async (req, reply) => {
const user = req.user
@@ -190,13 +300,11 @@ export default async function wikiRoutes (server: FastifyInstance) {
.delete(wikiPages)
.where(and(
eq(wikiPages.id, id),
eq(wikiPages.tenantId, user.tenant_id) // WICHTIG: Security
eq(wikiPages.tenantId, user.tenant_id)
))
.returning({ id: wikiPages.id })
if (result.length === 0) {
return reply.code(404).send({ error: "Not found or not authorized" })
}
if (result.length === 0) return reply.code(404).send({ error: "Not found" })
return { success: true, deletedId: result[0].id }
})