Zum Inhalt springen

OBS/Kostenpflichtige Module/RESTServer/Beispiel3: Unterschied zwischen den Versionen

Aus OBS Wiki
Rademacker (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Rademacker (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
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-Methoden (GET, POST, PUT, DELETE). Es geht um Tickets eines externen Servicedesks, der Tickets ueber den OBS REST-Server in OBS einliefern, abrufen, aktualisieren und schliessen kann.
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 ''oParams.Values['_OBS_PATH_id']'' bereit. Ein vollständiges Beispiel dazu zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].}}
{{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>
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Hilfsfunktion: einzelnes Ticket als JSON-Objekt aufbauen
// 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.
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


function _TicketAsJSON(qTicket: TxFQuery): TJSONObject;
procedure _TicketFelder(oWriter: TxRestWriter; qTicket: TxFQuery;
                        const cOffset: string);
begin
begin
     result := TJSONObject.Create();
     oWriter.Str ('id'     , qTicket.A2UID());
    result.AddPair('id'       , qTicket.A2UID());
     oWriter.Str ('nr'     , qTicket.A2C('ti_nr'));
     result.AddPair('nr'       , qTicket.A2C('ti_nr'));
     oWriter.Str ('betreff', qTicket.A2C('ti_betreff'));
     result.AddPair('betreff'   , qTicket.A2C('ti_betreff'));
     oWriter.Str ('status' , qTicket.A2C('ti_status'));
     result.AddPair('status'   , qTicket.A2C('ti_status'));
     oWriter.Str ('beschr' , qTicket.A2C('ti_beschr'));
     result.AddPair('beschr'   , qTicket.A2C('ti_beschr'));
     // Zeitpunkte gehen als ISO-8601 MIT OFFSET ueber die Leitung, nicht im
     result.AddPair('aenderung' , DTToSQL(qTicket.A2D('ti_aend_dat')));
    // 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
// Hilfsfunktion: ein Feld aus dem JSON-Body als String lesen.
// Hilfsfunktionen mehr: oReader liest typisiert, oWriter.Error baut die
// Generics gibt es im Skript nicht - oBody.TryGetValue<string>(...) laesst sich
// vollstaendige Fehlerhuelle samt traceId.
// nicht uebersetzen. GetValue liefert nil, wenn das Feld fehlt.
//------------------------------------------------------------------------------
 
function _BodyStr(oBody: TJSONObject; const cFeld: string): string;
var oVal: TJSONValue;
begin
    result := '';
    if (oBody = nil) then begin
        exit;
    end;
    oVal := oBody.GetValue(cFeld);
    if (Assigned(oVal)) then begin
        result := oVal.Value;
    end;
end;
 
//------------------------------------------------------------------------------
// Hilfsfunktion: Fehlerantwort im einheitlichen Format aufbauen.
// Fachliche Ablehnungen als 4xx melden, nicht als 200 mit Fehlertext.
//------------------------------------------------------------------------------
 
function _Fehler(oParams: TStrings; nStatus: integer; const cCode, cMsg: string): string;
var oRes: TJSONObject;
    oErr: TJSONObject;
begin
    oErr := TJSONObject.Create();
    oErr.AddPair('code'  , cCode);
    oErr.AddPair('message', cMsg);
    oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
 
    oRes := TJSONObject.Create();
    try
        oRes.AddPair('_OBS_HTTP_STATUS', nStatus);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;


//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
Zeile 83: Zeile 51:
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


function Get(oParams: TStrings; oBody: TJSONObject): string;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);
var cSql : string;
var cSql     : string;
     qData : TxFQuery;
     qData   : TxFQuery;
     oArr : TJSONArray;
     cId      : string;
     cId  : string;
    cOffset : string;
     lGefunden: Boolean;
begin
begin
     cId := oParams.Values['id'];
     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
             result := _TicketAsJSON(qData).ToJSON();
             // EoF gehoert dazu: DB_SOpen sagt nur, dass die ABFRAGE lief.
        end else begin
            if (not qData.EoF) then begin
            result := _Fehler(oParams, 404, 'NOT_FOUND', 'Ticket nicht gefunden');
                _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
         oArr := TJSONArray.Create();
         // Die Liste ist eine nackte Liste. RootArr steht vor der Abfrage:
         try
        // kein Treffer heisst leere Liste, nicht leeres Objekt.
            cSql := 'SELECT * FROM tickets WHERE ti_status <> "9" ORDER BY ti_aend_dat DESC';
        oWriter.RootArr();
            if (DB_SOpen(oDB, cSql, qData)) then begin
         cSql := 'SELECT * FROM tickets WHERE ti_status <> "9" ORDER BY ti_aend_dat DESC';
                while (not qData.EoF) do begin
        if (DB_SOpen(oDB, cSql, qData)) then begin
                    oArr.Add(_TicketAsJSON(qData));
            while (not qData.EoF) do begin
                    qData.Next();
                oWriter.ObjBegin('');
                end;
                    _TicketFelder(oWriter, qData, cOffset);
                oWriter.ObjEnd;
                qData.Next();
             end;
             end;
            DB_Close(qData);
            result := oArr.ToJSON();
        finally
            MyFreeAndNil(oArr);
         end;
         end;
        DB_Close(qData);
     end;
     end;
end;
end;
Zeile 123: Zeile 99:
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


function Post(oParams: TStrings; oBody: TJSONObject): string;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket : TqSQL;
var xTicket : TqSQL;
     cId    : string;
     cId    : string;
Zeile 129: Zeile 105:
     cBeschr : string;
     cBeschr : string;
     lOk    : Boolean;
     lOk    : Boolean;
    oRes    : TJSONObject;
begin
begin
     if (not Assigned(oBody)) then begin
    // HasBody unterscheidet "kein Koerper" von "Koerper ohne das Feld" - das
         result := _Fehler(oParams, 400, 'BAD_REQUEST', 'JSON-Body erforderlich');
    // 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 := _BodyStr(oBody, 'betreff');
     cBetreff := '';
     cBeschr  := _BodyStr(oBody, 'beschr');
     cBeschr  := '';
    oReader.Str('betreff', cBetreff);
    oReader.Str('beschr' , cBeschr);


     if (Empty(cBetreff)) then begin
     if (Empty(cBetreff)) then begin
         result := _Fehler(oParams, 422, 'VALIDATION_FAILED', 'Feld "betreff" fehlt');
         oWriter.Error(422, 'VALIDATION_FAILED', 'Feld "betreff" fehlt');
         exit;
         exit;
     end;
     end;
Zeile 166: Zeile 145:


     if (not lOk) then begin
     if (not lOk) then begin
         result := _Fehler(oParams, 500, 'INTERNAL_ERROR', 'Ticket konnte nicht angelegt werden');
         oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht angelegt werden');
         exit;
         exit;
     end;
     end;


     oRes := TJSONObject.Create();
     oWriter.Str('id'    , cId);
    try
    oWriter.Str('status', 'created');
        oRes.AddPair('id'    , cId);
    oWriter.Status(201);
        oRes.AddPair('status', 'created');
     oWriter.Header('Location', '/tickets/v1?id=' + cId);
        result := oRes.ToJSON();
     finally
        MyFreeAndNil(oRes);
    end;
end;
end;


Zeile 185: Zeile 160:
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


function Put(oParams: TStrings; oBody: TJSONObject): string;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket: TqSQL;
var xTicket: TqSQL;
     lOk    : Boolean;
     lOk    : Boolean;
Zeile 192: Zeile 167:
     cBeschr: string;
     cBeschr: string;
begin
begin
     if (not Assigned(oBody)) then begin
     if (not oReader.HasBody()) then begin
         result := _Fehler(oParams, 400, 'BAD_REQUEST', 'JSON-Body erforderlich');
         oWriter.Error(400, 'BAD_REQUEST', 'JSON-Body erforderlich');
         exit;
         exit;
     end;
     end;


     cId    := _BodyStr(oBody, 'id');
     cId    := '';
     cStat  := _BodyStr(oBody, 'status');
     cStat  := '';
     cBeschr := _BodyStr(oBody, 'beschr');
    cBeschr := '';
    oReader.Str('id'    , cId);
    oReader.Str('status', cStat);
     oReader.Str('beschr', cBeschr);


     if (Empty(cId)) then begin
     if (Empty(cId)) then begin
         result := _Fehler(oParams, 422, 'VALIDATION_FAILED', 'Feld "id" fehlt');
         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
         result := _Fehler(oParams, 404, 'NOT_FOUND', 'Ticket nicht gefunden');
         oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden');
         exit;
         exit;
     end;
     end;
Zeile 226: Zeile 204:


     if (not lOk) then begin
     if (not lOk) then begin
         result := _Fehler(oParams, 500, 'INTERNAL_ERROR', 'Ticket konnte nicht geaendert werden');
         oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geaendert werden');
         exit;
         exit;
     end;
     end;


     result := '{"status":"updated"}';
     oWriter.Str('status', 'updated');
end;
end;


//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// DELETE /tickets/v1?id=... - Ticket schliessen (Soft-Delete via Status=9)
// 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.
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


function Delete(oParams: TStrings; oBody: TJSONObject): string;
procedure Patch(oReader: TxRestReader; oWriter: TxRestWriter);
var xTicket: TqSQL;
var xTicket: TqSQL;
     cId    : string;
     cId    : string;
     lOk    : Boolean;
     lOk    : Boolean;
begin
begin
     cId := oParams.Values['id'];
     cId := oReader.Param('id');
     if (Empty(cId)) then begin
     if (Empty(cId)) then begin
         result := _Fehler(oParams, 422, 'VALIDATION_FAILED', 'Parameter "id" fehlt');
         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
         result := _Fehler(oParams, 404, 'NOT_FOUND', 'Ticket nicht gefunden');
         oWriter.Error(404, 'NOT_FOUND', 'Ticket nicht gefunden');
         exit;
         exit;
     end;
     end;
Zeile 264: Zeile 246:


     if (not lOk) then begin
     if (not lOk) then begin
         result := _Fehler(oParams, 500, 'INTERNAL_ERROR', 'Ticket konnte nicht geschlossen werden');
         oWriter.Error(500, 'INTERNAL_ERROR', 'Ticket konnte nicht geschlossen werden');
         exit;
         exit;
     end;
     end;


     result := '{"status":"closed"}';
     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 296: Zeile 288:
Ticket schliessen:
Ticket schliessen:


  curl -X DELETE -H "apikey: [API-KEY]" "https://api.meinserver.de/tickets/v1?id=abc-123"
  curl -X PATCH -H "apikey: [API-KEY]" "https://api.meinserver.de/tickets/v1?id=abc-123"


==Aufruf aus PHP==
==Aufruf aus PHP==
Zeile 343: 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, DELETE).
* 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

Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen



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.

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 Beispiel 4 - Pfad-Parameter.

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;
ACHTUNG: Ein DELETE-Endpunkt kann derzeit kein Skript haben. Der Server

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

der Engine.

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).