Zum Inhalt springen

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

Aus OBS Wiki
Böhrer (Diskussion | Beiträge)
Die Seite wurde neu angelegt: „Monster Legends is an island builder that permits the player to gather monsters and practice them for battles in the wild or against players. The characters a…“
 
Rademacker (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
(8 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Monster Legends is an island builder that permits the player to gather monsters and practice them for battles in the wild or against players. The characters are each sturdy and quite a few, but whereas there are countless forged members, unlike many shows where there are giant amounts of characters which are completely [https://www.Change.org/search?q=useless useless] and contribute nothing to the plot, every character in Monster brings one thing to the mix.<br><br>Rustic wood incorporates many natural traits including open and closed knots, pitch pockets, rasped edges, gouges, wormholes, small season checks, mineral streaks, dramatic shade [http://Www.europeana.eu/portal/search.html?query=distinction distinction] and different pure variations all reflecting the inherent beauty of the wooden.<br><br>It can be tempting to get caught up in the fun of breeding and elevating monsters, with out paying enough attention to the talents that you have unlocked for each of them (or the talents that you still have not unlocked!) Every monster has a different [http://get-gems.postbit.com/emotions-around-the-monster-legends-hack.html cheat in monster legends gems] assortment of abilities, and if you wish to reach battles-rather than just sitting around raising monsters all day-then you definately'll need to know what they're capable of!<br><br>To actually turn into the stuff of [http://mombao3.livejournal.com/620.html legends of tomorrow cast amaya], you will have to use the free BlueStacks Android Emulator to play Monster Legends on COMPUTER or Mac. The supporting solid is just as essential as a result of they are the means that help this anime arrive at a profitable end.<br><br>What separates Rings from other puzzle video games is it appears to do an excellent job of giving you a way out when the board is almost full. The level meter on the prime of the sport display screen exhibits how many XP you've collected and the remaining XP it is advisable to stage up.
{{Kostenpflichtige Module}}
 
=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 [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|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''==
 
<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
    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;
</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==
 
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==
 
<syntaxhighlight lang="php" line>
<?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;
}
?>
</syntaxhighlight>
 
==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 [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4]]).

Aktuelle Version vom 9. September 2026, 13:31 Uhr

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