feat: API v1 fuer Aufgaben
Lesen, erfassen, aendern — mehr nicht. Status, Bereich und Projekt lassen sich von aussen nicht setzen: Was hereinkommt, landet in der Inbox und wird hier einsortiert, sonst raeumt ein fremder Dienst die eigene Ordnung um. Mit einer Kennung ist das Erfassen wiederholbar: Zweimal dasselbe ergibt eine Aufgabe, nicht zwei. Das ist der Unterschied zwischen einer Schnittstelle, die man gefahrlos wiederholen kann, und einer, die bei jedem Netzhaenger Karteileichen anlegt. Jede schreibende Nutzung steht im Protokoll, mitsamt dem Token. Lesen nicht: Ein Dienst, der im Minutentakt abfragt, wuerde das Protokoll unbrauchbar machen. Alle Fehlschlaege sehen von aussen gleich aus. Ob ein Token unbekannt, abgelaufen oder widerrufen ist, geht niemanden etwas an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d5c7974af3
commit
6d3ebd05ae
@@ -0,0 +1,143 @@
|
||||
import { aenderungEingabe } from '@/lib/api'
|
||||
import type { Prisma } from '@/generated/prisma/client'
|
||||
import { db } from '@/server/db'
|
||||
import { abgelehnt, apiProtokoll, daten, rumpfLesen, wache } from '@/server/api/wache'
|
||||
import { jetzt } from '@/server/zeitgeber'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Eine einzelne Aufgabe lesen und ändern.
|
||||
*
|
||||
* Ändern heißt: Titel, Notiz, Fälligkeit, Priorität, Aufwand — und abhaken.
|
||||
* Bereich und Projekt bleiben außen vor; die Einordnung ist Sache dessen, der
|
||||
* plant, nicht dessen, der meldet.
|
||||
*/
|
||||
|
||||
const AUSWAHL = {
|
||||
id: true,
|
||||
titel: true,
|
||||
notiz: true,
|
||||
status: true,
|
||||
prioritaet: true,
|
||||
aufwandMinuten: true,
|
||||
faelligAm: true,
|
||||
quelle: true,
|
||||
quelleRef: true,
|
||||
quelleUrl: true,
|
||||
erstelltAm: true,
|
||||
bereich: { select: { name: true } },
|
||||
projekt: { select: { name: true } },
|
||||
} as const
|
||||
|
||||
type Zeile = {
|
||||
id: string
|
||||
titel: string
|
||||
notiz: string | null
|
||||
status: string
|
||||
prioritaet: number
|
||||
aufwandMinuten: number | null
|
||||
faelligAm: Date | null
|
||||
quelle: string
|
||||
quelleRef: string | null
|
||||
quelleUrl: string | null
|
||||
erstelltAm: Date
|
||||
bereich: { name: string } | null
|
||||
projekt: { name: string } | null
|
||||
}
|
||||
|
||||
function alsAntwort(a: Zeile) {
|
||||
return {
|
||||
id: a.id,
|
||||
titel: a.titel,
|
||||
notiz: a.notiz,
|
||||
status: a.status,
|
||||
prioritaet: a.prioritaet,
|
||||
aufwandMinuten: a.aufwandMinuten,
|
||||
faelligAm: a.faelligAm ? a.faelligAm.toISOString() : null,
|
||||
quelle: a.quelle,
|
||||
ref: a.quelleRef,
|
||||
url: a.quelleUrl,
|
||||
erstelltAm: a.erstelltAm.toISOString(),
|
||||
bereich: a.bereich?.name ?? null,
|
||||
projekt: a.projekt?.name ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
anfrage: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const geprueft = await wache(anfrage, 'lesen')
|
||||
if (!geprueft.ok) return geprueft.antwort
|
||||
|
||||
const { id } = await params
|
||||
const aufgabe = await db.aufgabe.findUnique({ where: { id }, select: AUSWAHL })
|
||||
if (!aufgabe) return abgelehnt('nichtGefunden')
|
||||
|
||||
return daten(alsAntwort(aufgabe))
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
anfrage: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const geprueft = await wache(anfrage, 'schreiben')
|
||||
if (!geprueft.ok) return geprueft.antwort
|
||||
const { aufrufer } = geprueft
|
||||
|
||||
const { id } = await params
|
||||
const rumpf = await rumpfLesen(anfrage)
|
||||
if (rumpf === undefined) return abgelehnt('ungueltig')
|
||||
|
||||
const gelesen = aenderungEingabe.safeParse(rumpf)
|
||||
if (!gelesen.success) {
|
||||
return daten(
|
||||
{
|
||||
fehler: 'Die Anfrage war fehlerhaft.',
|
||||
felder: gelesen.error.issues.map((f) => ({
|
||||
feld: f.path.join('.'),
|
||||
grund: f.message,
|
||||
})),
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
const vorhanden = await db.aufgabe.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, status: true },
|
||||
})
|
||||
if (!vorhanden) return abgelehnt('nichtGefunden')
|
||||
|
||||
const { erledigt, faelligAm, titel, notiz, prioritaet, aufwandMinuten } = gelesen.data
|
||||
const zeitpunkt = jetzt()
|
||||
|
||||
// Feld fuer Feld statt Spread: Mit exactOptionalPropertyTypes ist ein
|
||||
// gesetztes "undefined" etwas anderes als ein fehlendes Feld, und Prisma
|
||||
// wuerde daraus einen Schreibversuch machen.
|
||||
const werte: Prisma.AufgabeUpdateInput = {}
|
||||
if (titel !== undefined) werte.titel = titel
|
||||
if (notiz !== undefined) werte.notiz = notiz
|
||||
if (prioritaet !== undefined) werte.prioritaet = prioritaet
|
||||
if (aufwandMinuten !== undefined) werte.aufwandMinuten = aufwandMinuten
|
||||
if (faelligAm !== undefined) {
|
||||
werte.faelligAm = faelligAm === null ? null : new Date(faelligAm)
|
||||
}
|
||||
if (erledigt !== undefined) {
|
||||
werte.status = erledigt ? 'ERLEDIGT' : 'OFFEN'
|
||||
werte.erledigtAm = erledigt ? zeitpunkt : null
|
||||
}
|
||||
|
||||
const aufgabe = await db.aufgabe.update({
|
||||
where: { id: vorhanden.id },
|
||||
data: werte,
|
||||
select: AUSWAHL,
|
||||
})
|
||||
|
||||
await apiProtokoll(aufrufer, 'api.geaendert', aufgabe.id, {
|
||||
felder: Object.keys(gelesen.data),
|
||||
})
|
||||
|
||||
return daten(alsAntwort(aufgabe))
|
||||
}
|
||||
Reference in New Issue
Block a user