OBS/Kostenpflichtige Module/RESTServer/Beispiel3: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| (Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt) | |||
| Zeile 3: | Zeile 3: | ||
=Beispiel 3: Datensatz anlegen mit JSON-Body= | =Beispiel 3: Datensatz anlegen mit JSON-Body= | ||
Dieses Beispiel zeigt einen Endpunkt mit allen vier CRUD- | Dieses Beispiel zeigt einen Endpunkt mit allen vier CRUD-Zugriffen (GET, POST, PUT und - anstelle von DELETE - PATCH, siehe den Hinweis beim Schliessen). Es geht um Tickets eines externen Servicedesks, der Tickets ueber den OBS REST-Server in OBS einliefern, abrufen, aktualisieren und schliessen kann. | ||
{{Hinweis|Dieses Beispiel adressiert ein einzelnes Ticket über einen Query-Parameter (''?id...''). Alternativ lässt sich dieselbe Ressource über ein Pfad-Template wie ''/tickets/{id}'' ansprechen; die ID steht dann als '' | {{Hinweis|Dieses Beispiel adressiert ein einzelnes Ticket über einen Query-Parameter (''?id...''). Alternativ lässt sich dieselbe Ressource über ein Pfad-Template wie ''/tickets/{id}'' ansprechen; die ID steht dann als ''oReader.Path('id')'' bereit. Ein vollständiges Beispiel dazu zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].}} | ||
==Einrichtung in OBS== | ==Einrichtung in OBS== | ||
| Zeile 21: | Zeile 21: | ||
<syntaxhighlight lang="pascal" line> | <syntaxhighlight lang="pascal" line> | ||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
// | // Hilfsprozedur: die FELDER eines Tickets. Sie bekommt den Writer und schreibt | ||
// in das Objekt, das der Aufrufer geoeffnet hat - einmal als Listeneintrag, | |||
// einmal in die Wurzel. Ein Helfer, der ein Fragment als String zurueckgibt, | |||
// waere der alte Weg; dabei ging die Reihenfolge im Zusammenbau verloren. | |||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
procedure _TicketFelder(oWriter: TxRestWriter; qTicket: TxFQuery; | |||
const cOffset: string); | |||
begin | begin | ||
oWriter.Str ('id' , qTicket.A2UID()); | |||
oWriter.Str ('nr' , qTicket.A2C('ti_nr')); | |||
oWriter.Str ('betreff', qTicket.A2C('ti_betreff')); | |||
oWriter.Str ('status' , qTicket.A2C('ti_status')); | |||
oWriter.Str ('beschr' , qTicket.A2C('ti_beschr')); | |||
// Zeitpunkte gehen als ISO-8601 MIT OFFSET ueber die Leitung, nicht im | |||
// SQL-Format: DTToSQL liefert 'JJJJ-MM-TT hh:mm:ss' ohne Zeitzone, und der | |||
// Konsument muss dann raten, in welcher er steht. RestIsoFromDate nimmt den | |||
// Offset des Servers dazu. Ein leeres Datum ergibt null, nicht 1899. | |||
oWriter.StrN('aenderung', RestIsoFromDate(qTicket.A2D('ti_aend_dat'), cOffset)); | |||
end; | end; | ||
//- | // Fuer den Body-Zugriff und fuer Fehlerantworten braucht es keine eigenen | ||
// Hilfsfunktionen mehr: oReader liest typisiert, oWriter.Error baut die | |||
// vollstaendige Fehlerhuelle samt traceId. | |||
// | |||
// | |||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
| Zeile 83: | Zeile 51: | ||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter); | |||
var cSql | var cSql : string; | ||
qData : TxFQuery; | qData : TxFQuery; | ||
cId : string; | |||
cOffset : string; | |||
lGefunden: Boolean; | |||
begin | begin | ||
cId := | cId := oReader.Param('id'); | ||
// Der Offset des Servers geht in jeden Zeitstempel der Antwort. | |||
cOffset := oReader.Offset(); | |||
if (not Empty(cId)) then begin | if (not Empty(cId)) then begin | ||
lGefunden := false; | |||
cSql := 'SELECT * FROM tickets WHERE sys_uid = ' + DB_SQLVal(cId); | cSql := 'SELECT * FROM tickets WHERE sys_uid = ' + DB_SQLVal(cId); | ||
if (DB_SOpen(oDB, cSql, qData)) then begin | if (DB_SOpen(oDB, cSql, qData)) then begin | ||
// EoF gehoert dazu: DB_SOpen sagt nur, dass die ABFRAGE lief. | |||
if (not qData.EoF) then begin | |||
_TicketFelder(oWriter, qData, cOffset); | |||
lGefunden := true; | |||
end; | |||
end; | end; | ||
DB_Close(qData); | DB_Close(qData); | ||
if (not lGefunden) then begin | |||
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden'); | |||
end; | |||
end else begin | end else begin | ||
// Die Liste ist eine nackte Liste. RootArr steht vor der Abfrage: | |||
// kein Treffer heisst leere Liste, nicht leeres Objekt. | |||
oWriter.RootArr(); | |||
cSql := 'SELECT * FROM tickets WHERE ti_status <> "9" ORDER BY ti_aend_dat DESC'; | |||
if (DB_SOpen(oDB, cSql, qData)) then begin | |||
while (not qData.EoF) do begin | |||
oWriter.ObjBegin(''); | |||
_TicketFelder(oWriter, qData, cOffset); | |||
oWriter.ObjEnd; | |||
qData.Next(); | |||
end; | end; | ||
end; | end; | ||
DB_Close(qData); | |||
end; | end; | ||
end; | end; | ||
| Zeile 123: | Zeile 99: | ||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter); | |||
var xTicket : TqSQL; | var xTicket : TqSQL; | ||
cId : string; | cId : string; | ||
cBetreff: string; | cBetreff: string; | ||
cBeschr : string; | cBeschr : string; | ||
lOk : Boolean; | |||
begin | begin | ||
if (not | // HasBody unterscheidet "kein Koerper" von "Koerper ohne das Feld" - das | ||
// erste ist ein 400, das zweite ein 422. | |||
if (not oReader.HasBody()) then begin | |||
oWriter.Error(400, 'BAD_REQUEST', 'JSON-Body erforderlich'); | |||
exit; | exit; | ||
end; | end; | ||
cBetreff := | cBetreff := ''; | ||
cBeschr := | cBeschr := ''; | ||
oReader.Str('betreff', cBetreff); | |||
oReader.Str('beschr' , cBeschr); | |||
if (Empty(cBetreff)) then begin | if (Empty(cBetreff)) then begin | ||
oWriter.Error(422, 'VALIDATION_FAILED', 'Feld "betreff" fehlt'); | |||
exit; | exit; | ||
end; | end; | ||
| Zeile 155: | Zeile 135: | ||
xTicket.qSet('ti_anl_dat' , Now()); | xTicket.qSet('ti_anl_dat' , Now()); | ||
xTicket.qSet('ti_aend_dat', Now()); | xTicket.qSet('ti_aend_dat', Now()); | ||
xTicket.SaveData(NEW_RECORD); | // SaveData liefert einen Boolean. Ein SQL-Fehler - etwa Duplicate entry | ||
// auf einem eindeutigen Index - wirft KEINE Exception und steht in | |||
// keinem Protokoll. Ohne diese Auswertung antwortet der Endpunkt mit | |||
// 201, ohne etwas angelegt zu haben. | |||
lOk := xTicket.SaveData(NEW_RECORD); | |||
finally | finally | ||
qSqlFree(xTicket); | qSqlFree(xTicket); | ||
end; | end; | ||
if (not lOk) then begin | |||
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht angelegt werden'); | |||
exit; | |||
end; | end; | ||
oWriter.Str('id' , cId); | |||
oWriter.Str('status', 'created'); | |||
oWriter.Status(201); | |||
oWriter.Header('Location', '/tickets/v1?id=' + cId); | |||
end; | end; | ||
| Zeile 175: | Zeile 160: | ||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter); | |||
var xTicket: TqSQL; | var xTicket: TqSQL; | ||
lOk : Boolean; | |||
cId : string; | cId : string; | ||
cStat : string; | cStat : string; | ||
cBeschr: string; | cBeschr: string; | ||
begin | begin | ||
if (not | if (not oReader.HasBody()) then begin | ||
oWriter.Error(400, 'BAD_REQUEST', 'JSON-Body erforderlich'); | |||
exit; | exit; | ||
end; | end; | ||
cId := | cId := ''; | ||
cStat := | cStat := ''; | ||
cBeschr := ''; | |||
oReader.Str('id' , cId); | |||
oReader.Str('status', cStat); | |||
oReader.Str('beschr', cBeschr); | |||
if (Empty(cId)) then begin | if (Empty(cId)) then begin | ||
oWriter.Error(422, 'VALIDATION_FAILED', 'Feld "id" fehlt'); | |||
exit; | exit; | ||
end; | end; | ||
if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin | if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin | ||
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden'); | |||
exit; | exit; | ||
end; | end; | ||
xTicket := | // Zum Aendern qSqlInit mit sys_uid, nicht qSqlRead: qSqlRead liest den | ||
// Altsatz und schreibt nur Aenderungen - ein unveraenderter Wert erzeugt | |||
// gar kein Write und SaveData liefert False. | |||
xTicket := qSqlInit(oDB, 'tickets'); | |||
xTicket.qSet('sys_uid', cId); | |||
try | try | ||
if (not Empty(cStat)) then xTicket.qSet('ti_status' , cStat); | if (not Empty(cStat)) then xTicket.qSet('ti_status' , cStat); | ||
if (not Empty(cBeschr)) then xTicket.qSet('ti_beschr' , cBeschr); | if (not Empty(cBeschr)) then xTicket.qSet('ti_beschr' , cBeschr); | ||
xTicket.qSet('ti_aend_dat', Now()); | xTicket.qSet('ti_aend_dat', Now()); | ||
xTicket.SaveData(UPDATE_RECORD); | lOk := xTicket.SaveData(UPDATE_RECORD); | ||
finally | finally | ||
qSqlFree(xTicket); | qSqlFree(xTicket); | ||
end; | end; | ||
if (not lOk) then begin | |||
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geaendert werden'); | |||
exit; | |||
end; | |||
oWriter.Str('status', 'updated'); | |||
end; | end; | ||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
// | // PATCH /tickets/v1?id=... - Ticket schliessen (Soft-Delete via Status=9) | ||
// | |||
// NICHT als DELETE: 'Delete' ist in der Skriptsprache eine Standardprozedur, | |||
// eine eigene Prozedur dieses Namens laesst sich nicht uebersetzen. Solange das | |||
// so ist, laeuft ein Loeschen ueber PATCH oder POST. Siehe den Hinweis unten. | |||
//------------------------------------------------------------------------------ | //------------------------------------------------------------------------------ | ||
procedure Patch(oReader: TxRestReader; oWriter: TxRestWriter); | |||
var xTicket: TqSQL; | var xTicket: TqSQL; | ||
cId : string; | cId : string; | ||
lOk : Boolean; | |||
begin | begin | ||
cId := | cId := oReader.Param('id'); | ||
if (Empty(cId)) then begin | if (Empty(cId)) then begin | ||
oWriter.Error(422, 'VALIDATION_FAILED', 'Parameter "id" fehlt'); | |||
exit; | exit; | ||
end; | end; | ||
if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin | if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin | ||
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden'); | |||
exit; | exit; | ||
end; | end; | ||
xTicket := | xTicket := qSqlInit(oDB, 'tickets'); | ||
xTicket.qSet('sys_uid', cId); | |||
try | try | ||
xTicket.qSet('ti_status' , '9'); | xTicket.qSet('ti_status' , '9'); | ||
xTicket.qSet('ti_aend_dat', Now()); | xTicket.qSet('ti_aend_dat', Now()); | ||
xTicket.SaveData(UPDATE_RECORD); | lOk := xTicket.SaveData(UPDATE_RECORD); | ||
finally | finally | ||
qSqlFree(xTicket); | qSqlFree(xTicket); | ||
end; | end; | ||
if (not lOk) then begin | |||
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geschlossen werden'); | |||
exit; | |||
end; | |||
oWriter.Str('status', 'closed'); | |||
end; | end; | ||
</syntaxhighlight> | </syntaxhighlight> | ||
{{Achtung|'''Ein DELETE-Endpunkt kann derzeit kein Skript haben.''' Der Server | |||
ruft die Prozedur, die genauso heisst wie das HTTP-Verb - fuer DELETE also | |||
<code>Delete</code>. Dieser Name ist in der Skriptsprache belegt | |||
(Standardprozedur zum Loeschen aus einer Zeichenkette), und die Deklaration | |||
laesst sich nicht uebersetzen; die Meldung nennt dabei nur die Zeile, nicht die | |||
Ursache. Bis das geloest ist: das Loeschen als <code>PATCH</code> oder | |||
<code>POST</code> mit einer Aktion abbilden. Das Abmelden am JWT-Endpunkt ist | |||
davon nicht betroffen - es laeuft als DELETE '''ohne''' Skript vollstaendig in | |||
der Engine.}} | |||
==Aufruf mit curl== | ==Aufruf mit curl== | ||
| Zeile 269: | Zeile 288: | ||
Ticket schliessen: | Ticket schliessen: | ||
curl -X | curl -X PATCH -H "apikey: [API-KEY]" "https://api.meinserver.de/tickets/v1?id=abc-123" | ||
==Aufruf aus PHP== | ==Aufruf aus PHP== | ||
| Zeile 316: | Zeile 335: | ||
* Saubere Trennung der CRUD-Methoden in einem Endpunkt. | * Saubere Trennung der CRUD-Methoden in einem Endpunkt. | ||
* Verwendung des JSON-Body fuer komplexere Eingangsdaten (POST, PUT). | * Verwendung des JSON-Body fuer komplexere Eingangsdaten (POST, PUT). | ||
* Verwendung von Query-Parametern fuer einfache Werte (GET, | * Verwendung von Query-Parametern fuer einfache Werte (GET, PATCH). | ||
* Soft-Delete ueber Status-Aenderung statt physischer Loeschung. | * Soft-Delete ueber Status-Aenderung statt physischer Loeschung - und warum er als PATCH und nicht als DELETE laeuft. | ||
* Eindeutige Audit-UIDs (''Y00CXXXXxx'') fuer jeden DB-Zugriff zur Nachvollziehbarkeit. | * Eindeutige Audit-UIDs (''Y00CXXXXxx'') fuer jeden DB-Zugriff zur Nachvollziehbarkeit. | ||
* Host-gebundener Zugang als zweite Sicherheitsebene neben dem API-Key. | * Host-gebundener Zugang als zweite Sicherheitsebene neben dem API-Key. | ||
* Alternativ: ID per Pfad-Parameter statt Query-Parameter (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4]]). | * Alternativ: ID per Pfad-Parameter statt Query-Parameter (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4]]). | ||
Aktuelle Version vom 9. September 2026, 13:31 Uhr
- A Preise aktualisieren
- C Personen übertragen
- E Kategorien verwalten
- G Kataloge verwalten
- I Merkliste übertragen
- K Varianten übertragen
- L Artikelvarianten übertragen
- M Referenzarten übertragen
- N Lagerbestände verwalten
- U Bestellungen einlesen
- V leere Passworte füllen
- W Update-Informationen zurücksetzen
- X Konfiguration
- Z Protokoll
Beispiel 3: Datensatz anlegen mit JSON-Body
Dieses Beispiel zeigt einen Endpunkt mit allen vier CRUD-Zugriffen (GET, POST, PUT und - anstelle von DELETE - PATCH, siehe den Hinweis beim Schliessen). Es geht um Tickets eines externen Servicedesks, der Tickets ueber den OBS REST-Server in OBS einliefern, abrufen, aktualisieren und schliessen kann.
Einrichtung in OBS
- Server-Profil: Standard-TLS-Profil Public-API, Bindung 0.0.0.0:443
- Zugang: Servicedesk-X
- API-Key: zufaellig generiert
- Host: servicedesk.kunde.de (DNS-gebunden, IP-Zugriff wird abgelehnt)
- JWT: nicht aktiv
- Endpunkt: tickets/v1, Profil Public-API
- Berechtigung: Zugang Servicedesk-X fuer Endpunkt tickets/v1 freigeschaltet
Endpunkt-Skript tickets/v1
//------------------------------------------------------------------------------
// Hilfsprozedur: die FELDER eines Tickets. Sie bekommt den Writer und schreibt
// in das Objekt, das der Aufrufer geoeffnet hat - einmal als Listeneintrag,
// einmal in die Wurzel. Ein Helfer, der ein Fragment als String zurueckgibt,
// waere der alte Weg; dabei ging die Reihenfolge im Zusammenbau verloren.
//------------------------------------------------------------------------------
procedure _TicketFelder(oWriter: TxRestWriter; qTicket: TxFQuery;
const cOffset: string);
begin
oWriter.Str ('id' , qTicket.A2UID());
oWriter.Str ('nr' , qTicket.A2C('ti_nr'));
oWriter.Str ('betreff', qTicket.A2C('ti_betreff'));
oWriter.Str ('status' , qTicket.A2C('ti_status'));
oWriter.Str ('beschr' , qTicket.A2C('ti_beschr'));
// Zeitpunkte gehen als ISO-8601 MIT OFFSET ueber die Leitung, nicht im
// SQL-Format: DTToSQL liefert 'JJJJ-MM-TT hh:mm:ss' ohne Zeitzone, und der
// Konsument muss dann raten, in welcher er steht. RestIsoFromDate nimmt den
// Offset des Servers dazu. Ein leeres Datum ergibt null, nicht 1899.
oWriter.StrN('aenderung', RestIsoFromDate(qTicket.A2D('ti_aend_dat'), cOffset));
end;
// Fuer den Body-Zugriff und fuer Fehlerantworten braucht es keine eigenen
// Hilfsfunktionen mehr: oReader liest typisiert, oWriter.Error baut die
// vollstaendige Fehlerhuelle samt traceId.
//------------------------------------------------------------------------------
// GET /tickets/v1?id=... - einzelnes Ticket
// GET /tickets/v1 - alle offenen Tickets
//------------------------------------------------------------------------------
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);
var cSql : string;
qData : TxFQuery;
cId : string;
cOffset : string;
lGefunden: Boolean;
begin
cId := oReader.Param('id');
// Der Offset des Servers geht in jeden Zeitstempel der Antwort.
cOffset := oReader.Offset();
if (not Empty(cId)) then begin
lGefunden := false;
cSql := 'SELECT * FROM tickets WHERE sys_uid = ' + DB_SQLVal(cId);
if (DB_SOpen(oDB, cSql, qData)) then begin
// EoF gehoert dazu: DB_SOpen sagt nur, dass die ABFRAGE lief.
if (not qData.EoF) then begin
_TicketFelder(oWriter, qData, cOffset);
lGefunden := true;
end;
end;
DB_Close(qData);
if (not lGefunden) then begin
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden');
end;
end else begin
// Die Liste ist eine nackte Liste. RootArr steht vor der Abfrage:
// kein Treffer heisst leere Liste, nicht leeres Objekt.
oWriter.RootArr();
cSql := 'SELECT * FROM tickets WHERE ti_status <> "9" ORDER BY ti_aend_dat DESC';
if (DB_SOpen(oDB, cSql, qData)) then begin
while (not qData.EoF) do begin
oWriter.ObjBegin('');
_TicketFelder(oWriter, qData, cOffset);
oWriter.ObjEnd;
qData.Next();
end;
end;
DB_Close(qData);
end;
end;
//------------------------------------------------------------------------------
// POST /tickets/v1 - neues Ticket anlegen
// Body: { "betreff":"...", "beschr":"..." }
//------------------------------------------------------------------------------
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket : TqSQL;
cId : string;
cBetreff: string;
cBeschr : string;
lOk : Boolean;
begin
// HasBody unterscheidet "kein Koerper" von "Koerper ohne das Feld" - das
// erste ist ein 400, das zweite ein 422.
if (not oReader.HasBody()) then begin
oWriter.Error(400, 'BAD_REQUEST', 'JSON-Body erforderlich');
exit;
end;
cBetreff := '';
cBeschr := '';
oReader.Str('betreff', cBetreff);
oReader.Str('beschr' , cBeschr);
if (Empty(cBetreff)) then begin
oWriter.Error(422, 'VALIDATION_FAILED', 'Feld "betreff" fehlt');
exit;
end;
cId := GetNewId(oDB);
xTicket := qSqlInit(oDB, 'tickets');
xTicket.lNoSysUID := True;
try
xTicket.qSet('sys_uid' , cId);
xTicket.qSet('ti_nr' , DB_NeuNum(oDB, 'tickets', 'ti_nr', NEUNUM_HOLE, '', '', 1, 999999, '0'));
xTicket.qSet('ti_betreff' , cBetreff);
xTicket.qSet('ti_beschr' , cBeschr);
xTicket.qSet('ti_status' , '1');
xTicket.qSet('ti_anl_dat' , Now());
xTicket.qSet('ti_aend_dat', Now());
// SaveData liefert einen Boolean. Ein SQL-Fehler - etwa Duplicate entry
// auf einem eindeutigen Index - wirft KEINE Exception und steht in
// keinem Protokoll. Ohne diese Auswertung antwortet der Endpunkt mit
// 201, ohne etwas angelegt zu haben.
lOk := xTicket.SaveData(NEW_RECORD);
finally
qSqlFree(xTicket);
end;
if (not lOk) then begin
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht angelegt werden');
exit;
end;
oWriter.Str('id' , cId);
oWriter.Str('status', 'created');
oWriter.Status(201);
oWriter.Header('Location', '/tickets/v1?id=' + cId);
end;
//------------------------------------------------------------------------------
// PUT /tickets/v1 - Ticket aktualisieren
// Body: { "id":"...", "status":"...", "beschr":"..." }
//------------------------------------------------------------------------------
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket: TqSQL;
lOk : Boolean;
cId : string;
cStat : string;
cBeschr: string;
begin
if (not oReader.HasBody()) then begin
oWriter.Error(400, 'BAD_REQUEST', 'JSON-Body erforderlich');
exit;
end;
cId := '';
cStat := '';
cBeschr := '';
oReader.Str('id' , cId);
oReader.Str('status', cStat);
oReader.Str('beschr', cBeschr);
if (Empty(cId)) then begin
oWriter.Error(422, 'VALIDATION_FAILED', 'Feld "id" fehlt');
exit;
end;
if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden');
exit;
end;
// Zum Aendern qSqlInit mit sys_uid, nicht qSqlRead: qSqlRead liest den
// Altsatz und schreibt nur Aenderungen - ein unveraenderter Wert erzeugt
// gar kein Write und SaveData liefert False.
xTicket := qSqlInit(oDB, 'tickets');
xTicket.qSet('sys_uid', cId);
try
if (not Empty(cStat)) then xTicket.qSet('ti_status' , cStat);
if (not Empty(cBeschr)) then xTicket.qSet('ti_beschr' , cBeschr);
xTicket.qSet('ti_aend_dat', Now());
lOk := xTicket.SaveData(UPDATE_RECORD);
finally
qSqlFree(xTicket);
end;
if (not lOk) then begin
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geaendert werden');
exit;
end;
oWriter.Str('status', 'updated');
end;
//------------------------------------------------------------------------------
// PATCH /tickets/v1?id=... - Ticket schliessen (Soft-Delete via Status=9)
//
// NICHT als DELETE: 'Delete' ist in der Skriptsprache eine Standardprozedur,
// eine eigene Prozedur dieses Namens laesst sich nicht uebersetzen. Solange das
// so ist, laeuft ein Loeschen ueber PATCH oder POST. Siehe den Hinweis unten.
//------------------------------------------------------------------------------
procedure Patch(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket: TqSQL;
cId : string;
lOk : Boolean;
begin
cId := oReader.Param('id');
if (Empty(cId)) then begin
oWriter.Error(422, 'VALIDATION_FAILED', 'Parameter "id" fehlt');
exit;
end;
if (not DB_LSeek(oDB, 'tickets', 'sys_uid = ' + DB_SQLVal(cId))) then begin
oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden');
exit;
end;
xTicket := qSqlInit(oDB, 'tickets');
xTicket.qSet('sys_uid', cId);
try
xTicket.qSet('ti_status' , '9');
xTicket.qSet('ti_aend_dat', Now());
lOk := xTicket.SaveData(UPDATE_RECORD);
finally
qSqlFree(xTicket);
end;
if (not lOk) then begin
oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geschlossen werden');
exit;
end;
oWriter.Str('status', 'closed');
end;ruft die Prozedur, die genauso heisst wie das HTTP-Verb - fuer DELETE also
Delete. Dieser Name ist in der Skriptsprache belegt
(Standardprozedur zum Loeschen aus einer Zeichenkette), und die Deklaration
laesst sich nicht uebersetzen; die Meldung nennt dabei nur die Zeile, nicht die
Ursache. Bis das geloest ist: das Loeschen als PATCH oder
POST mit einer Aktion abbilden. Das Abmelden am JWT-Endpunkt ist
davon nicht betroffen - es laeuft als DELETE ohne Skript vollstaendig in
Aufruf mit curl
Liste aller offenen Tickets:
curl -H "apikey: [API-KEY]" https://api.meinserver.de/tickets/v1
Einzelnes Ticket:
curl -H "apikey: [API-KEY]" "https://api.meinserver.de/tickets/v1?id=abc-123"
Neues Ticket anlegen:
curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
-d "{\"betreff\":\"Drucker offline\",\"beschr\":\"Etage 2, Raum 23\"}" ^
https://api.meinserver.de/tickets/v1
Ticket aktualisieren:
curl -X PUT -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
-d "{\"id\":\"abc-123\",\"status\":\"2\"}" ^
https://api.meinserver.de/tickets/v1
Ticket schliessen:
curl -X PATCH -H "apikey: [API-KEY]" "https://api.meinserver.de/tickets/v1?id=abc-123"
Aufruf aus PHP
<?php
$apikey = '[API-KEY]';
$base = 'https://api.meinserver.de/tickets/v1';
function rest($method, $url, $apikey, $body = null) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL , $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST , $method);
$headers = ['apikey: ' . $apikey];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$res = curl_exec($ch);
curl_close($ch);
return json_decode($res, true);
}
// Neues Ticket anlegen
$neu = rest('POST', $base, $apikey, [
'betreff' => 'Drucker offline',
'beschr' => 'Etage 2, Raum 23'
]);
$id = $neu['id'];
// Status aktualisieren
rest('PUT', $base, $apikey, ['id' => $id, 'status' => '2']);
// Liste auslesen
$alle = rest('GET', $base, $apikey);
foreach ($alle as $t) {
echo $t['nr'] . ' - ' . $t['betreff'] . PHP_EOL;
}
?>Was zeigt das Beispiel?
- Saubere Trennung der CRUD-Methoden in einem Endpunkt.
- Verwendung des JSON-Body fuer komplexere Eingangsdaten (POST, PUT).
- Verwendung von Query-Parametern fuer einfache Werte (GET, PATCH).
- Soft-Delete ueber Status-Aenderung statt physischer Loeschung - und warum er als PATCH und nicht als DELETE laeuft.
- Eindeutige Audit-UIDs (Y00CXXXXxx) fuer jeden DB-Zugriff zur Nachvollziehbarkeit.
- Host-gebundener Zugang als zweite Sicherheitsebene neben dem API-Key.
- Alternativ: ID per Pfad-Parameter statt Query-Parameter (siehe Beispiel 4).