OBS/Kostenpflichtige Module/RESTServer/Scripting: Unterschied zwischen den Versionen
(Die Seite wurde neu angelegt: „{{Kostenpflichtige Module}} =Anleitung für Endpunkt-Scripte= Anfragen an einen Endpunkt werden (nach erfolgreicher Authentifizierung und Authorisierung) an das hinterlegte Script weitergegeben und müssen dort beantwortet werden. Gibt es einen Fehler oder wird die Anfrage nicht korrekt bearbeitet wird ein Fehlercode (HTML-Fehlercode) zurückgegeben. =Sicherheit= Da hier ein Zugriff von außen ermöglicht wird ist es sehr wichtig, dass Übergabeparame…“) |
Keine Bearbeitungszusammenfassung |
||
| (7 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
{{Kostenpflichtige Module}} | {{Kostenpflichtige Module}} | ||
=Anleitung für Endpunkt- | =Anleitung für Endpunkt-Skripte= | ||
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu. | |||
= | ==Sicherheitshinweise== | ||
{{Hinweis|Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über ''DB_SQLVal'' bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.}} | |||
* Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche). | |||
* Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter. | |||
* Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in '''RESTSRV_PROTO'''. | |||
==Methoden-Signatur== | |||
function | |||
begin | Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt. | ||
[ | |||
function Get (oParams: TStrings; oBody: TJSONObject): string; | |||
function Post (oParams: TStrings; oBody: TJSONObject): string; | |||
function Put (oParams: TStrings; oBody: TJSONObject): string; | |||
function Delete(oParams: TStrings; oBody: TJSONObject): string; | |||
function Patch (oParams: TStrings; oBody: TJSONObject): string; | |||
Nicht implementierte Methoden liefern automatisch ''405''-ähnliche Fehler über die generische Skript-Antwort. | |||
==Parameter== | |||
===oParams (TStrings)=== | |||
Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings. | |||
Filterung durch den Server: | |||
* Geblockte Header werden nicht durchgereicht: ''authorization'', ''cookie'', ''proxy-authorization'', ''x-forwarded-for'', ''x-real-ip'', ''apikey'', ''api_key''. | |||
* Parameter mit Präfix '''_OBS_''' können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert. | |||
* Werte werden auf max. 1024 Zeichen begrenzt. | |||
* Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt. | |||
Zugriff im Skript: | |||
if (not Empty(oParams.Values['kundennr'])) then begin | |||
cKundenNr := oParams.Values['kundennr']; | |||
end; | end; | ||
===oBody (TJSONObject)=== | |||
begin | |||
Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird ''nil'' übergeben. | |||
cUser := ''; | |||
cPass := ''; | |||
if (Assigned(oBody)) then begin | |||
oVal := oBody.GetValue('username'); | |||
if (Assigned(oVal)) then begin | |||
cUser := oVal.Value; | |||
end; | |||
oVal := oBody.GetValue('password'); | |||
if (Assigned(oVal)) then begin | |||
cPass := oVal.Value; | |||
end; | |||
end; | end; | ||
function | Maximale Body-Grösse: 10 MB. | ||
begin | |||
[...] | ===Reservierte _OBS_-Parameter=== | ||
end; | |||
Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung: | |||
{| class="wikitable" | |||
! Parameter !! Inhalt | |||
|- | |||
| _OBS_JWT_ID || JWT-Id (''jti''-Claim) - typisch die User-Id | |||
|- | |||
| _OBS_JWT_SUBJECT || Subject (''sub''-Claim) - typisch Benutzername | |||
|- | |||
| _OBS_JWT_AUDIENCE || Audience (''aud''-Claim) - typisch Mandant / Rolle | |||
|- | |||
| _OBS_JWT_CLAIM_<name> || Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar | |||
|- | |||
| _OBS_TRACE_ID || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id) | |||
|} | |||
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden. | |||
===Pfad-Parameter=== | |||
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix '''_OBS_PATH_''' in ''oParams'' bereit: | |||
{| class="wikitable" | |||
! Template !! Zugriff im Skript | |||
|- | |||
| <code>/orders/{uid}</code> || ''oParams.Values['_OBS_PATH_uid']'' | |||
|- | |||
| <code>/orders/{uid}/modules/{code}</code> || ''oParams.Values['_OBS_PATH_uid']'', ''oParams.Values['_OBS_PATH_code']'' | |||
|} | |||
Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]]. | |||
===Datei-Uploads=== | |||
Ist der Endpunkt für Uploads freigeschaltet (<code>re_upload = 1</code>, siehe | |||
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server | |||
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und | |||
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte | |||
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B. | |||
DMS-Ablage). Der Body wird in diesem Fall '''nicht''' als JSON geparst - ''oBody'' | |||
ist ''nil''. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro | |||
Endpunkt konfigurierte Grösse (''re_upload_size'', Standard 25 MB; | |||
Überschreitung -> 413). | |||
Beide Übertragungsarten - <code>multipart/form-data</code> und der resumable | |||
<code>Content-Range</code>-Upload - liefern dem Skript dieselben Parameter: | |||
{| class="wikitable" | |||
! Parameter !! Inhalt | |||
|- | |||
| _OBS_UPLOAD_PATH || Vollständiger Pfad zur temporären Datei auf dem Server | |||
|- | |||
| _OBS_UPLOAD_NAME || Originaldateiname | |||
|- | |||
| _OBS_UPLOAD_CONTENTTYPE || Content-Type der Datei (Default <code>application/octet-stream</code>, wenn der Client keinen angibt) | |||
|} | |||
Der '''Dateiname ist Pflicht'''. Fehlt er, lehnt der Server den Upload mit | |||
'''400 Bad Request''' ab und das Skript wird nicht ausgeführt - das Skript kann | |||
sich also darauf verlassen, dass ''_OBS_UPLOAD_PATH'' und ''_OBS_UPLOAD_NAME'' | |||
gesetzt sind, sobald es läuft. | |||
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}} | |||
====Einfacher Upload (multipart/form-data)==== | |||
Eine Datei pro Request. Name und Content-Type stammen aus der | |||
<code>Content-Disposition</code> der Datei-Partie (<code>filename=</code>). Der | |||
Server speichert die erste Datei-Partie und ruft das Skript auf: | |||
<syntaxhighlight lang="pascal" line> | |||
function Post(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes : TJSONObject; | |||
cPfad: string; | |||
cName: string; | |||
cTyp : string; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
cPfad := oParams.Values['_OBS_UPLOAD_PATH']; | |||
cName := oParams.Values['_OBS_UPLOAD_NAME']; | |||
cTyp := oParams.Values['_OBS_UPLOAD_CONTENTTYPE']; | |||
// ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ... | |||
oRes.AddPair('status' , 'ok'); | |||
oRes.AddPair('dateiname', cName); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
====Resumable/Chunked Upload (Content-Range)==== | |||
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem | |||
Header <code>Content-Range: bytes START-END/TOTAL</code>. Der Server hängt die | |||
Chunks '''sequentiell''' an eine Session-Datei an und behandelt unvollständige | |||
Uploads selbst - das Endpunkt-Skript läuft '''erst beim vollständigen Upload''' | |||
(dann mit gesetztem ''_OBS_UPLOAD_PATH'', ''_OBS_UPLOAD_NAME'' und | |||
''_OBS_UPLOAD_CONTENTTYPE''). | |||
Name und Content-Type sind im <code>Content-Range</code>-Protokoll nicht enthalten; | |||
der Client liefert sie deshalb '''im ersten Chunk''' über den Header | |||
<code>Upload-Metadata</code> (Format wie bei tus: kommagetrennte Paare | |||
<code>schlüssel base64wert</code>, Werte Base64-kodiert): | |||
<pre> | |||
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm | |||
</pre> | |||
Erkannt werden die Schlüssel <code>filename</code> (Pflicht) und | |||
<code>filetype</code> (optional). Fehlt <code>filename</code> im ersten Chunk, | |||
wird der Upload mit '''400''' abgelehnt, ohne dass eine Datei angelegt wird. | |||
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden. | |||
Ablauf (vom Client gesteuert, der Server antwortet jeweils): | |||
{| class="wikitable" | |||
! Request !! Server-Antwort | |||
|- | |||
| Erster Chunk <code>Content-Range: bytes 0-1048575/5000000</code> + <code>Upload-Metadata</code> || '''308''' Resume Incomplete + Header <code>Upload-Id</code>, <code>Upload-Offset</code>, <code>Range</code> | |||
|- | |||
| weitere Chunks (jeweils <code>Upload-Id</code> mitsenden) || '''308''' mit aktualisiertem <code>Upload-Offset</code> | |||
|- | |||
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. '''200'''/'''201''') | |||
|- | |||
| Statusabfrage <code>Content-Range: bytes */5000000</code> || aktueller <code>Upload-Offset</code>, ohne anzuhängen (Resume) | |||
|} | |||
{| class="wikitable" | |||
! Header !! Richtung !! Bedeutung | |||
|- | |||
| Content-Range || Request || <code>bytes START-END/TOTAL</code>; <code>bytes */TOTAL</code> nur als Statusabfrage | |||
|- | |||
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: <code>filename</code> (Pflicht), <code>filetype</code> (optional), Base64-kodiert | |||
|- | |||
| Upload-Id || Request/Response || Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden. | |||
|- | |||
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks) | |||
|- | |||
| Range || Response || Bereits gespeicherter Bereich (<code>bytes=0-N</code>) | |||
|} | |||
Der Server hängt einen Chunk '''nur an, wenn START dem bereits gespeicherten Stand | |||
entspricht'''. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, | |||
sondern der aktuelle ''Upload-Offset'' zurückgemeldet - der Client setzt ab dort | |||
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig. | |||
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (''re_upload_size''), | |||
antwortet der Server mit '''413 Payload Too Large'''. | |||
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}} | |||
==Rückgabe== | |||
Die Rückgabe ist immer ein '''JSON-String'''. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit ''{}''. Der Server setzt Content-Type auf ''application/json; charset=utf-8'' und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert. | |||
Einfaches Beispiel: | |||
<syntaxhighlight lang="pascal" line> | |||
function Get(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes: TJSONObject; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
oRes.AddPair('wert_string', '123'); | |||
oRes.AddPair('wert_int' , 456); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
==Antwort steuern: Statuscode, Header, traceId== | |||
Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body. | |||
{| class="wikitable" | |||
! Antwort-Feld !! Wirkung | |||
|- | |||
| _OBS_HTTP_STATUS (Zahl) || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet. | |||
|- | |||
| _OBS_HEADERS (Objekt) || Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection). | |||
|} | |||
Zusätzlich trÄgt jede Antwort den Header '''X-Trace-Id''' (Korrelations-ID). Dieselbe ID liegt dem Skript als ''oParams.Values['_OBS_TRACE_ID']'' vor und erscheint in jeder Server-Fehler-Logzeile in '''RESTSRV_PROTO''' - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden. | |||
===Beispiel: Anlegen mit 201, Location und ETag=== | |||
<syntaxhighlight lang="pascal" line> | |||
function Post(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes, oHdr: TJSONObject; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
// ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ... | |||
oHdr := TJSONObject.Create(); | |||
oHdr.AddPair('Location', '/v1/orders/' + cNeueUid); | |||
oHdr.AddPair('ETag', '1'); | |||
oRes.AddPair('_OBS_HTTP_STATUS', 201); | |||
oRes.AddPair('_OBS_HEADERS', oHdr); | |||
oRes.AddPair('uid', cNeueUid); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
===Beispiel: Strukturierter Fehler mit Statuscode und traceId=== | |||
<syntaxhighlight lang="pascal" line> | |||
function Post(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes, oErr: TJSONObject; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
oErr := TJSONObject.Create(); | |||
oErr.AddPair('code', 'VALIDATION_FAILED'); | |||
oErr.AddPair('message', 'Feld "menge" fehlt'); | |||
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']); | |||
oRes.AddPair('_OBS_HTTP_STATUS', 422); | |||
oRes.AddPair('error', oErr); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
===Beispiel: Optimistic Concurrency (ETag / If-Match)=== | |||
Mit Statuscode, Headern und dem lesbaren Header ''If-Match'' fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab: | |||
<syntaxhighlight lang="pascal" line> | |||
function Put(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes, oHdr: TJSONObject; | |||
nAktuell, nIfMatch: integer; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']); | |||
nIfMatch := iVal(oParams.Values['if-match']); | |||
if (nIfMatch <> nAktuell) then begin | |||
oHdr := TJSONObject.Create(); | |||
oHdr.AddPair('ETag', xStr(nAktuell)); | |||
oRes.AddPair('_OBS_HTTP_STATUS', 409); | |||
oRes.AddPair('_OBS_HEADERS', oHdr); | |||
oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT')); | |||
result := oRes.ToJSON(); | |||
exit; | |||
end; | |||
// ... speichern, Version hochzÄhlen ... | |||
oHdr := TJSONObject.Create(); | |||
oHdr.AddPair('ETag', xStr(nAktuell + 1)); | |||
oRes.AddPair('_OBS_HEADERS', oHdr); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
==JWT-Authentifizierungs-Skript== | |||
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode ''Authenticate'' bereitstellen: | |||
<syntaxhighlight lang="pascal" line> | |||
function Authenticate(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes : TJSONObject; | |||
cUser : string; | |||
cPass : string; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
// Eingangsdaten lesen (Body oder Query-Param) | |||
cUser := ''; | |||
cPass := ''; | |||
if (Assigned(oBody)) then begin | |||
oVal := oBody.GetValue('username'); | |||
if (Assigned(oVal)) then begin | |||
cUser := oVal.Value; | |||
end; | |||
oVal := oBody.GetValue('password'); | |||
if (Assigned(oVal)) then begin | |||
cPass := oVal.Value; | |||
end; | |||
end; | |||
// Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ... | |||
if (PasswortPasst(cUser, cPass)) then begin | |||
oRes.AddPair('status' , 1); | |||
oRes.AddPair('_OBS_JWT_ID' , cUser); | |||
oRes.AddPair('_OBS_JWT_SUBJECT', cUser); | |||
oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1'); | |||
end else begin | |||
oRes.AddPair('status', 9); | |||
oRes.AddPair('error' , 'Login fehlgeschlagen'); | |||
end; | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
Rückgabewerte: | |||
{| class="wikitable" | |||
! status !! Bedeutung | |||
|- | |||
| 1 || Erfolgreich, der Server erzeugt aus den ''_OBS_JWT_*''-Werten einen Token (HS256) | |||
|- | |||
| 9 || Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von ''error'' | |||
|} | |||
===Custom-Claims, serverTime und Refresh=== | |||
Das Authenticate-Skript kann dem Token zusÄtzlich '''beliebige Custom-Claims''' mitgeben - z.B. Mandant und Rollen - und optional ein '''Refresh-Token''' anstoßen: | |||
{| class="wikitable" | |||
! RÜckgabefeld !! Bedeutung | |||
|- | |||
| _OBS_JWT_CLAIM_<name> || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>']. | |||
|- | |||
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus. | |||
|- | |||
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600). | |||
|} | |||
Jedes Token trÄgt automatisch den Claim ''token_use'' (''access'' bzw. ''refresh''). Die Antwort enthÄlt bei Erfolg ''serverTime'' (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich ''refreshToken'': | |||
{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"} | |||
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (''_OBS_JWT_CLAIM_*''), nie aus Body oder Query.}} | |||
====Refresh-Methode==== | |||
Der Refresh lÄuft Über '''denselben JWT-Endpunkt''' und '''dasselbe Skript'''. Liegt am JWT-Endpunkt ein ''Authorization: Bearer <token>'' vor, ruft der Server statt ''Authenticate'' die Methode '''Refresh''' auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, ''token_use=refresh''); das alte Refresh-jti steht als ''oParams.Values['_OBS_JWT_ID']'' bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie ''Authenticate'' neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt. | |||
<syntaxhighlight lang="pascal" line> | |||
function Refresh(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes: TJSONObject; | |||
cAltesJti, cNeuesJti: string; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
cAltesJti := oParams.Values['_OBS_JWT_ID']; | |||
// altes Refresh-jti gegen eigene Sperrtabelle prÜfen | |||
if (not RefreshJtiGueltig(cAltesJti)) then begin | |||
oRes.AddPair('status', 9); | |||
oRes.AddPair('error' , 'AUTH_EXPIRED'); | |||
result := oRes.ToJSON(); | |||
exit; | |||
end; | |||
cNeuesJti := GlobalUID(); | |||
RefreshJtiRotieren(cAltesJti, cNeuesJti); // altes sperren, neues ablegen | |||
oRes.AddPair('status' , 1); | |||
oRes.AddPair('_OBS_JWT_ID' , GlobalUID()); | |||
oRes.AddPair('_OBS_JWT_SUBJECT' , oParams.Values['_OBS_JWT_SUBJECT']); | |||
oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']); | |||
oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']); | |||
oRes.AddPair('_OBS_JWT_REFRESH_ID' , cNeuesJti); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
==Fehlerbehandlung im Skript== | |||
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 ''Interner Fehler'' und protokolliert die Detail-Meldung in '''RESTSRV_PROTO''' (mit Skript-Fehlertext). Der Konsument sieht keine internen Details. | |||
Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, ist eine eigene JSON-Struktur zu liefern: | |||
=Parameter= | <syntaxhighlight lang="pascal" line> | ||
function Post(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes: TJSONObject; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
if (Empty(oParams.Values['kundennr'])) then begin | |||
oRes.AddPair('status', 'error'); | |||
oRes.AddPair('msg' , 'Parameter ''kundennr'' fehlt'); | |||
result := oRes.ToJSON(); | |||
exit; | |||
end; | |||
// ... | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
==Skript-Cache== | |||
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: ''sys_date'' des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort. | |||
= | ==Verfügbare Bibliotheken== | ||
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte: | |||
* ''Base.Tools'' - String-, Datum-, IIF-, Empty-Helper | |||
* ''Base.DB'' / ''Base.xQuery'' - Datenbank-Operationen, ''DB_SQLVal'', ''DB_SOpen'', ''DB_LSeek'' | |||
* ''Base.qSqlReg'' - ''qSqlInit'', ''qSet'', ''SaveData'' | |||
* ''System.JSON'' - JSON-Objekte und -Arrays | |||
* ''Base.ToolsConst'' - Konstanten wie ''CRLF'', ''SINGELQUOTE'', ''EMPTY_DATE'' | |||
Konkrete Beispiele: | |||
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel: Daten-Abruf mit JWT]] | |||
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel: Datensatz anlegen mit JSON-Body]] | |||
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel: Pfad-Parameter im Routing]] | |||
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel: Schreibzugriff mit Statuscodes, ETag und Idempotenz]] | |||
Aktuelle Version vom 30. Juni 2026, 12:28 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
Anleitung für Endpunkt-Skripte
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.
Sicherheitshinweise
- Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
- Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
- Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in RESTSRV_PROTO.
Methoden-Signatur
Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.
function Get (oParams: TStrings; oBody: TJSONObject): string; function Post (oParams: TStrings; oBody: TJSONObject): string; function Put (oParams: TStrings; oBody: TJSONObject): string; function Delete(oParams: TStrings; oBody: TJSONObject): string; function Patch (oParams: TStrings; oBody: TJSONObject): string;
Nicht implementierte Methoden liefern automatisch 405-ähnliche Fehler über die generische Skript-Antwort.
Parameter
oParams (TStrings)
Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.
Filterung durch den Server:
- Geblockte Header werden nicht durchgereicht: authorization, cookie, proxy-authorization, x-forwarded-for, x-real-ip, apikey, api_key.
- Parameter mit Präfix _OBS_ können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
- Werte werden auf max. 1024 Zeichen begrenzt.
- Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.
Zugriff im Skript:
if (not Empty(oParams.Values['kundennr'])) then begin
cKundenNr := oParams.Values['kundennr'];
end;
oBody (TJSONObject)
Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird nil übergeben.
cUser := ;
cPass := ;
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('username');
if (Assigned(oVal)) then begin
cUser := oVal.Value;
end;
oVal := oBody.GetValue('password');
if (Assigned(oVal)) then begin
cPass := oVal.Value;
end;
end;
Maximale Body-Grösse: 10 MB.
Reservierte _OBS_-Parameter
Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:
| Parameter | Inhalt |
|---|---|
| _OBS_JWT_ID | JWT-Id (jti-Claim) - typisch die User-Id |
| _OBS_JWT_SUBJECT | Subject (sub-Claim) - typisch Benutzername |
| _OBS_JWT_AUDIENCE | Audience (aud-Claim) - typisch Mandant / Rolle |
| _OBS_JWT_CLAIM_<name> | Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar |
| _OBS_TRACE_ID | Korrelations-ID des Requests (auch als Response-Header X-Trace-Id) |
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.
Pfad-Parameter
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe Endpunkte), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix _OBS_PATH_ in oParams bereit:
| Template | Zugriff im Skript |
|---|---|
/orders/{uid} |
oParams.Values['_OBS_PATH_uid'] |
/orders/{uid}/modules/{code} |
oParams.Values['_OBS_PATH_uid'], oParams.Values['_OBS_PATH_code'] |
Da der Präfix _OBS_ für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt Beispiel 4 - Pfad-Parameter.
Datei-Uploads
Ist der Endpunkt für Uploads freigeschaltet (re_upload = 1, siehe
Endpunkte), nimmt der Server
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B.
DMS-Ablage). Der Body wird in diesem Fall nicht als JSON geparst - oBody
ist nil. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro
Endpunkt konfigurierte Grösse (re_upload_size, Standard 25 MB;
Überschreitung -> 413).
Beide Übertragungsarten - multipart/form-data und der resumable
Content-Range-Upload - liefern dem Skript dieselben Parameter:
| Parameter | Inhalt |
|---|---|
| _OBS_UPLOAD_PATH | Vollständiger Pfad zur temporären Datei auf dem Server |
| _OBS_UPLOAD_NAME | Originaldateiname |
| _OBS_UPLOAD_CONTENTTYPE | Content-Type der Datei (Default application/octet-stream, wenn der Client keinen angibt)
|
Der Dateiname ist Pflicht. Fehlt er, lehnt der Server den Upload mit 400 Bad Request ab und das Skript wird nicht ausgeführt - das Skript kann sich also darauf verlassen, dass _OBS_UPLOAD_PATH und _OBS_UPLOAD_NAME gesetzt sind, sobald es läuft.
Einfacher Upload (multipart/form-data)
Eine Datei pro Request. Name und Content-Type stammen aus der
Content-Disposition der Datei-Partie (filename=). Der
Server speichert die erste Datei-Partie und ruft das Skript auf:
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
cPfad: string;
cName: string;
cTyp : string;
begin
oRes := TJSONObject.Create();
try
cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
cName := oParams.Values['_OBS_UPLOAD_NAME'];
cTyp := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];
// ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...
oRes.AddPair('status' , 'ok');
oRes.AddPair('dateiname', cName);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Resumable/Chunked Upload (Content-Range)
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem
Header Content-Range: bytes START-END/TOTAL. Der Server hängt die
Chunks sequentiell an eine Session-Datei an und behandelt unvollständige
Uploads selbst - das Endpunkt-Skript läuft erst beim vollständigen Upload
(dann mit gesetztem _OBS_UPLOAD_PATH, _OBS_UPLOAD_NAME und
_OBS_UPLOAD_CONTENTTYPE).
Name und Content-Type sind im Content-Range-Protokoll nicht enthalten;
der Client liefert sie deshalb im ersten Chunk über den Header
Upload-Metadata (Format wie bei tus: kommagetrennte Paare
schlüssel base64wert, Werte Base64-kodiert):
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm
Erkannt werden die Schlüssel filename (Pflicht) und
filetype (optional). Fehlt filename im ersten Chunk,
wird der Upload mit 400 abgelehnt, ohne dass eine Datei angelegt wird.
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.
Ablauf (vom Client gesteuert, der Server antwortet jeweils):
| Request | Server-Antwort |
|---|---|
Erster Chunk Content-Range: bytes 0-1048575/5000000 + Upload-Metadata |
308 Resume Incomplete + Header Upload-Id, Upload-Offset, Range
|
weitere Chunks (jeweils Upload-Id mitsenden) |
308 mit aktualisiertem Upload-Offset
|
| letzter Chunk (Bereich erreicht TOTAL) | normale Skript-Antwort (z.B. 200/201) |
Statusabfrage Content-Range: bytes */5000000 |
aktueller Upload-Offset, ohne anzuhängen (Resume)
|
| Header | Richtung | Bedeutung |
|---|---|---|
| Content-Range | Request | bytes START-END/TOTAL; bytes */TOTAL nur als Statusabfrage
|
| Upload-Metadata | Request | tus-Metadaten im ersten Chunk: filename (Pflicht), filetype (optional), Base64-kodiert
|
| Upload-Id | Request/Response | Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden. |
| Upload-Offset | Response | Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks) |
| Range | Response | Bereits gespeicherter Bereich (bytes=0-N)
|
Der Server hängt einen Chunk nur an, wenn START dem bereits gespeicherten Stand entspricht. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, sondern der aktuelle Upload-Offset zurückgemeldet - der Client setzt ab dort neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (re_upload_size), antwortet der Server mit 413 Payload Too Large.
Rückgabe
Die Rückgabe ist immer ein JSON-String. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit {}. Der Server setzt Content-Type auf application/json; charset=utf-8 und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.
Einfaches Beispiel:
function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
oRes := TJSONObject.Create();
try
oRes.AddPair('wert_string', '123');
oRes.AddPair('wert_int' , 456);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Antwort steuern: Statuscode, Header, traceId
Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.
| Antwort-Feld | Wirkung |
|---|---|
| _OBS_HTTP_STATUS (Zahl) | HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet. |
| _OBS_HEADERS (Objekt) | Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection). |
Zusätzlich trÄgt jede Antwort den Header X-Trace-Id (Korrelations-ID). Dieselbe ID liegt dem Skript als oParams.Values['_OBS_TRACE_ID'] vor und erscheint in jeder Server-Fehler-Logzeile in RESTSRV_PROTO - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden.
Beispiel: Anlegen mit 201, Location und ETag
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
oRes := TJSONObject.Create();
try
// ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
oHdr := TJSONObject.Create();
oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
oHdr.AddPair('ETag', '1');
oRes.AddPair('_OBS_HTTP_STATUS', 201);
oRes.AddPair('_OBS_HEADERS', oHdr);
oRes.AddPair('uid', cNeueUid);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Beispiel: Strukturierter Fehler mit Statuscode und traceId
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
oRes := TJSONObject.Create();
try
oErr := TJSONObject.Create();
oErr.AddPair('code', 'VALIDATION_FAILED');
oErr.AddPair('message', 'Feld "menge" fehlt');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 422);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Beispiel: Optimistic Concurrency (ETag / If-Match)
Mit Statuscode, Headern und dem lesbaren Header If-Match fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab:
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
nAktuell, nIfMatch: integer;
begin
oRes := TJSONObject.Create();
try
nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
nIfMatch := iVal(oParams.Values['if-match']);
if (nIfMatch <> nAktuell) then begin
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nAktuell));
oRes.AddPair('_OBS_HTTP_STATUS', 409);
oRes.AddPair('_OBS_HEADERS', oHdr);
oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
result := oRes.ToJSON();
exit;
end;
// ... speichern, Version hochzÄhlen ...
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nAktuell + 1));
oRes.AddPair('_OBS_HEADERS', oHdr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
JWT-Authentifizierungs-Skript
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode Authenticate bereitstellen:
function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
cUser : string;
cPass : string;
begin
oRes := TJSONObject.Create();
try
// Eingangsdaten lesen (Body oder Query-Param)
cUser := '';
cPass := '';
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('username');
if (Assigned(oVal)) then begin
cUser := oVal.Value;
end;
oVal := oBody.GetValue('password');
if (Assigned(oVal)) then begin
cPass := oVal.Value;
end;
end;
// Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
if (PasswortPasst(cUser, cPass)) then begin
oRes.AddPair('status' , 1);
oRes.AddPair('_OBS_JWT_ID' , cUser);
oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
end else begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'Login fehlgeschlagen');
end;
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Rückgabewerte:
| status | Bedeutung |
|---|---|
| 1 | Erfolgreich, der Server erzeugt aus den _OBS_JWT_*-Werten einen Token (HS256) |
| 9 | Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von error |
Custom-Claims, serverTime und Refresh
Das Authenticate-Skript kann dem Token zusÄtzlich beliebige Custom-Claims mitgeben - z.B. Mandant und Rollen - und optional ein Refresh-Token anstoßen:
| RÜckgabefeld | Bedeutung |
|---|---|
| _OBS_JWT_CLAIM_<name> | Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>']. |
| _OBS_JWT_REFRESH_ID | (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus. |
| _OBS_JWT_REFRESH_EXP | (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600). |
Jedes Token trÄgt automatisch den Claim token_use (access bzw. refresh). Die Antwort enthÄlt bei Erfolg serverTime (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich refreshToken:
{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
Refresh-Methode
Der Refresh lÄuft Über denselben JWT-Endpunkt und dasselbe Skript. Liegt am JWT-Endpunkt ein Authorization: Bearer <token> vor, ruft der Server statt Authenticate die Methode Refresh auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, token_use=refresh); das alte Refresh-jti steht als oParams.Values['_OBS_JWT_ID'] bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie Authenticate neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt.
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
cAltesJti, cNeuesJti: string;
begin
oRes := TJSONObject.Create();
try
cAltesJti := oParams.Values['_OBS_JWT_ID'];
// altes Refresh-jti gegen eigene Sperrtabelle prÜfen
if (not RefreshJtiGueltig(cAltesJti)) then begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'AUTH_EXPIRED');
result := oRes.ToJSON();
exit;
end;
cNeuesJti := GlobalUID();
RefreshJtiRotieren(cAltesJti, cNeuesJti); // altes sperren, neues ablegen
oRes.AddPair('status' , 1);
oRes.AddPair('_OBS_JWT_ID' , GlobalUID());
oRes.AddPair('_OBS_JWT_SUBJECT' , oParams.Values['_OBS_JWT_SUBJECT']);
oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
oRes.AddPair('_OBS_JWT_REFRESH_ID' , cNeuesJti);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Fehlerbehandlung im Skript
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 Interner Fehler und protokolliert die Detail-Meldung in RESTSRV_PROTO (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.
Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, ist eine eigene JSON-Struktur zu liefern:
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
oRes := TJSONObject.Create();
try
if (Empty(oParams.Values['kundennr'])) then begin
oRes.AddPair('status', 'error');
oRes.AddPair('msg' , 'Parameter ''kundennr'' fehlt');
result := oRes.ToJSON();
exit;
end;
// ...
finally
MyFreeAndNil(oRes);
end;
end;
Skript-Cache
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: sys_date des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.
Verfügbare Bibliotheken
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:
- Base.Tools - String-, Datum-, IIF-, Empty-Helper
- Base.DB / Base.xQuery - Datenbank-Operationen, DB_SQLVal, DB_SOpen, DB_LSeek
- Base.qSqlReg - qSqlInit, qSet, SaveData
- System.JSON - JSON-Objekte und -Arrays
- Base.ToolsConst - Konstanten wie CRLF, SINGELQUOTE, EMPTY_DATE
Konkrete Beispiele: