Zum Inhalt

Eigene REST-Schnittstellen

Bisher haben die Skripte andere Systeme aufgerufen. Mit einem REST-Service ist es umgekehrt: Andere Systeme — ein Portal, eine Fachanwendung, ein Automatisierungs-Werkzeug — rufen KCForms auf, etwa um Daten abzufragen oder einen Antrag anzulegen.

Ein REST-Service ist ein kleines Skript-Modul. Angelegt wird es unter System-Administration → Schnittstellen & Verteilung → REST-Services; nach dem Speichern ist es sofort erreichbar. Ausführliches dazu: REST-Services.

So liest du die Beispiele

Jedes Beispiel ist ein vollständiger Service zum Einfügen in den Editor. Darunter steht, wie man ihn aufruft — zum Beispiel mit curl oder direkt in der eingebauten Swagger-UI (/api/rest/docs). <server> steht für die Adresse deiner KCForms-Installation.

Die Adresse eines Aufrufs setzt sich zusammen aus /api/rest/service + path des Service + path der Operation.

Nur für Administratoren

Ein REST-Service darf alles, was ein Skript darf — Datenbank, Dateien, Mails. Den Aufruf sicherst du je Service ab (Beispiele 15 und 16).

Einsteiger

1. Hallo Welt

module.exports = {
  path: "/beispiel/hallo",
  operations: [
    {
      id: "hallo",
      method: "GET",
      handler: function () {
        return { nachricht: "Hallo Welt" };
      }
    }
  ]
};

Aufruf: curl http://<server>/api/rest/service/beispiel/hallo → {"nachricht":"Hallo Welt"}

Ein zurückgegebenes Objekt wird automatisch zu JSON.

2. Einen Parameter mitgeben

Angaben hinter dem ? der Adresse stehen im Objekt query.

module.exports = {
  path: "/beispiel/gruss",
  operations: [
    {
      id: "gruss",
      method: "GET",
      handler: function () {
        return { text: "Hallo, " + (query.name || "Unbekannt") + "!" };
      }
    }
  ]
};

Aufruf: …/beispiel/gruss?name=Anna

3. Ein Teil der Adresse als Wert

{nr} im Pfad ist ein Platzhalter — sein Wert steht in pathParameters.

module.exports = {
  path: "/beispiel/artikel",
  operations: [
    {
      id: "artikel",
      path: "/{nr}",
      method: "GET",
      handler: function () {
        return { nummer: pathParameters.nr, bezeichnung: "Artikel " + pathParameters.nr };
      }
    }
  ]
};

Aufruf: …/beispiel/artikel/4711

4. Einfacher Text statt JSON

Gibt der Handler einen Text zurück, kommt genau dieser Text an.

module.exports = {
  path: "/beispiel/ping",
  operations: [
    { id: "ping", method: "GET", handler: function () { return "pong"; } }
  ]
};

5. Die Uhrzeit des Servers

module.exports = {
  path: "/beispiel/zeit",
  operations: [
    {
      id: "zeit",
      method: "GET",
      handler: function () {
        var jetzt = new Date();
        return { datum: jetzt.format("dd.MM.yyyy"), uhrzeit: jetzt.format("HH:mm:ss") };
      }
    }
  ]
};

6. Mehrere Operationen in einem Service

Ein Service kann viele Adressen bedienen — hier eine Liste und einen einzelnen Eintrag.

var farben = ["rot", "gruen", "blau"];

module.exports = {
  path: "/beispiel/farben",
  operations: [
    { id: "alle",  method: "GET", handler: function () { return farben; } },
    { id: "eine",  method: "GET", path: "/{nr}",
      handler: function () { return { farbe: farben[Number(pathParameters.nr)] }; } }
  ]
};

Hilfsvariablen wie farben stehen einfach oberhalb von module.exports.

Fortgeschritten

7. Daten empfangen (POST)

Was der Aufrufer mitschickt, steht in body. body.asString() liefert den Text, JSON.parse macht daraus ein Objekt.

module.exports = {
  path: "/beispiel/rechner",
  operations: [
    {
      id: "addieren",
      method: "POST",
      handler: function () {
        var daten = JSON.parse(body.asString());
        return { summe: daten.a + daten.b };
      }
    }
  ]
};

Aufruf: curl -X POST -H "Content-Type: application/json" -d '{"a":12,"b":30}' http://<server>/api/rest/service/beispiel/rechner

8. Fehlende Angaben zurückweisen

Response.badRequest(…) antwortet mit Status 400 („fehlerhafte Anfrage"). Mit throw bricht der Handler dabei sofort ab.

module.exports = {
  path: "/beispiel/anmeldung",
  operations: [
    {
      id: "anmelden",
      method: "POST",
      handler: function () {
        var daten = body.isEmpty() ? {} : JSON.parse(body.asString());
        if (!daten.name) {
          throw Response.badRequest({ fehler: "Name fehlt" });
        }
        return { ok: true, willkommen: daten.name };
      }
    }
  ]
};

9. „Nicht gefunden" melden

Für Unbekanntes gibt es Status 404.

var laender = { DE: "Deutschland", AT: "Österreich", SE: "Schweden" };

module.exports = {
  path: "/beispiel/land",
  operations: [
    {
      id: "land",
      path: "/{code}",
      method: "GET",
      handler: function () {
        var name = laender[pathParameters.code];
        if (!name) throw Response.notFound({ fehler: "Unbekannter Ländercode" });
        return { code: pathParameters.code, name: name };
      }
    }
  ]
};

10. Aus der Datenbank lesen

Alle Werkzeuge der anderen Kapitel stehen auch hier bereit.

module.exports = {
  path: "/beispiel/statistik",
  operations: [
    {
      id: "statistik",
      method: "GET",
      handler: function () {
        return {
          benutzer: kc.num(db.scalar("SELECT COUNT(*) FROM USERS")),
          stand:    new Date().format("yyyy-MM-dd HH:mm")
        };
      }
    }
  ]
};

11. Suchen mit Parameter

Werte aus der Anfrage gehören nie direkt in den SQL-Text — das ? ist ein sicherer Platzhalter.

module.exports = {
  path: "/beispiel/benutzer",
  operations: [
    {
      id: "suche",
      method: "GET",
      handler: function () {
        var begriff = "%" + (query.suche || "") + "%";
        var zeilen = db.query(
          "SELECT LOGIN_NAME FROM USERS WHERE LOGIN_NAME LIKE ? ORDER BY LOGIN_NAME", begriff);
        return zeilen.slice(0, 20).map(function (z) { return z.login_name; });
      }
    }
  ]
};

12. Eine CSV-Datei liefern

Mit Response.ok(…).type(…) bestimmst du den Dateityp selbst.

module.exports = {
  path: "/beispiel/export",
  operations: [
    {
      id: "export",
      method: "GET",
      handler: function () {
        var text = "Artikel;Menge\nStift;10\nBlock;5\n";
        return Response.ok(text)
          .type("text/csv; charset=UTF-8")
          .header("Content-Disposition", "attachment; filename=\"export.csv\"");
      }
    }
  ]
};

13. Kopfzeilen des Aufrufers lesen

HTTP-Kopfzeilen („Header") stehen in headers — Namen immer klein geschrieben.

module.exports = {
  path: "/beispiel/sprache",
  operations: [
    {
      id: "gruss",
      method: "GET",
      handler: function () {
        var sprache = String(headers["accept-language"] || "de").substring(0, 2);
        var gruesse = { de: "Hallo", en: "Hello", sv: "Hej" };
        return { gruss: gruesse[sprache] || gruesse.de };
      }
    }
  ]
};

14. „Angelegt" mit Status 201

Wird etwas neu angelegt, ist 201 Created die passende Antwort.

module.exports = {
  path: "/beispiel/ticket",
  operations: [
    {
      id: "anlegen",
      method: "POST",
      handler: function () {
        var daten = JSON.parse(body.asString());
        var ticket = { nummer: "T-" + Date.now(), titel: daten.titel, status: "offen" };
        return Response.created(ticket);
      }
    }
  ]
};

15. Mit Token absichern

Stelle im Editor unter Authentifizierung „Bearer-Token" ein und trage einen Token ein. Ohne diesen Token antwortet der Service mit 401.

module.exports = {
  path: "/beispiel/geheim",
  operations: [
    {
      id: "geheim",
      method: "GET",
      handler: function () {
        return { daten: "nur mit Token", aufrufer: request.user };
      }
    }
  ]
};

Aufruf: curl -H "Authorization: Bearer <token>" http://<server>/api/rest/service/beispiel/geheim

16. Mit Benutzer und Passwort absichern

„Basic" arbeitet mit Benutzername und Passwort — sie gehören zum Service, es sind keine KCForms-Konten. request.user enthält den Namen.

module.exports = {
  path: "/beispiel/intern",
  operations: [
    {
      id: "intern",
      method: "GET",
      handler: function () {
        return { angemeldet_als: request.user };
      }
    }
  ]
};

Aufruf: curl -u portal:<passwort> http://<server>/api/rest/service/beispiel/intern

Profi

Das Übungsformular

Die folgenden Beispiele speichern echte Datensätze im Formular uebung_bestellung mit den Feldern kunde (Text, Pflicht), betrag (Zahl), status (Auswahl neu/freigegeben/erledigt) und datum (Datum) — siehe Datensätze.

Jeder Aufruf beginnt von vorn

Variablen im Modul werden bei jedem Aufruf neu angelegt. Was bleiben soll, speicherst du mit forms oder db.

17. Einen Datensatz anlegen

Ein Portal übergibt eine Bestellung; KCForms legt den Datensatz an und meldet die neue Nummer zurück.

module.exports = {
  path: "/beispiel/bestellung",
  operations: [
    {
      id: "anlegen",
      method: "POST",
      handler: function () {
        var daten = JSON.parse(body.asString());
        var id = forms.create("uebung_bestellung", {
          kunde:  daten.kunde,
          betrag: daten.betrag,
          status: "neu",
          datum:  new Date().format("yyyy-MM-dd")
        });
        return Response.created({ id: id });
      }
    }
  ]
};

18. Ungültige Daten abweisen

forms.validate prüft gegen das Formular. Status 422 heißt: „Ich habe dich verstanden, aber die Daten passen nicht."

module.exports = {
  path: "/beispiel/bestellung",
  operations: [
    {
      id: "anlegen",
      method: "POST",
      handler: function () {
        var daten = body.isEmpty() ? {} : JSON.parse(body.asString());
        var werte = { kunde: daten.kunde, betrag: daten.betrag, status: "neu" };

        var fehler = forms.validate("uebung_bestellung", werte);
        if (fehler.length > 0) {
          throw Response.status(422, { fehler: fehler });
        }
        return Response.created({ id: forms.create("uebung_bestellung", werte) });
      }
    }
  ]
};

19. Lesen, Ändern, Löschen

Ein vollständiger Service für Bestellungen: Liste, einzelner Datensatz, Status ändern, löschen — je nach HTTP-Methode.

var FORM = "uebung_bestellung";

function holeOder404(id) {
  var satz = forms.get(FORM, Number(id));
  if (!satz) throw Response.notFound({ fehler: "Bestellung " + id + " gibt es nicht" });
  return satz;
}

module.exports = {
  path: "/beispiel/bestellungen",
  operations: [
    { id: "liste", method: "GET",
      handler: function () {
        return forms.list(FORM).map(function (s) {
          return { id: s.id, kunde: s.kunde, betrag: s.betrag, status: s.status };
        });
      } },
    { id: "eine", method: "GET", path: "/{id}",
      handler: function () { return holeOder404(pathParameters.id); } },
    { id: "status", method: "PATCH", path: "/{id}",
      handler: function () {
        holeOder404(pathParameters.id);
        var neu = JSON.parse(body.asString()).status;
        forms.update(FORM, Number(pathParameters.id), { status: neu }, { validate: true });
        return holeOder404(pathParameters.id);
      } },
    { id: "loeschen", method: "DELETE", path: "/{id}",
      handler: function () {
        holeOder404(pathParameters.id);
        forms.delete(FORM, Number(pathParameters.id));
        return Response.noContent();
      } }
  ]
};
Aufruf Wirkung
GET …/beispiel/bestellungen alle Bestellungen
GET …/beispiel/bestellungen/7 Bestellung 7
PATCH …/beispiel/bestellungen/7 mit {"status":"erledigt"} Status ändern
DELETE …/beispiel/bestellungen/7 löschen (Antwort 204, ohne Inhalt)

20. Konflikte melden

Status 409 („Konflikt") passt, wenn etwas schon existiert.

module.exports = {
  path: "/beispiel/kunde",
  operations: [
    {
      id: "anlegen",
      method: "POST",
      handler: function () {
        var name = JSON.parse(body.asString()).kunde;
        var vorhanden = forms.list("uebung_bestellung").some(function (s) { return s.kunde === name; });
        if (vorhanden) {
          throw Response.status(409, { fehler: "Kunde " + name + " ist schon vorhanden" });
        }
        return Response.created({ id: forms.create("uebung_bestellung", { kunde: name, status: "neu" }) });
      }
    }
  ]
};

21. Fehler mit eigenem Fehlercode

Für aufrufende Programme ist ein fester Fehlercode leichter auszuwerten als ein Text. Zusätzliche Felder am Fehler werden mitgeschickt.

module.exports = {
  path: "/beispiel/limit",
  operations: [
    {
      id: "pruefen",
      method: "GET",
      handler: function () {
        var betrag = Number(query.betrag || 0);
        if (betrag > 1000) {
          var e = new Error("Betrag über dem Limit von 1.000 €");
          e.code = "LIMIT_UEBERSCHRITTEN";
          e.limit = 1000;
          throw e;
        }
        return { ok: true, betrag: betrag };
      }
    }
  ]
};

Ein throw new Error(…) führt zu Status 500. Soll es ein anderer Status sein, nimm Response.status(…) wie in Beispiel 20.

22. Die Schnittstelle beschreiben

summary, description und die Parameter-Angaben erscheinen in der Swagger-UI unter /api/rest/docs — so können andere die Schnittstelle ohne Rückfrage nutzen.

module.exports = {
  path: "/beispiel/umrechnen",
  documentation: {
    info: { title: "Währungsrechner", version: "1.0", description: "Rechnet Euro in Kronen um." }
  },
  operations: [
    {
      id: "umrechnen",
      method: "GET",
      summary: "Euro in Kronen umrechnen",
      description: "Multipliziert den Euro-Betrag mit dem angegebenen Kurs.",
      tags: ["rechner"],
      queryParameters: [
        { name: "euro", schema: { type: "number" }, description: "Betrag in Euro" },
        { name: "kurs", schema: { type: "number" }, description: "Kronen je Euro" }
      ],
      handler: function () {
        var euro = Number(query.euro), kurs = Number(query.kurs);
        if (isNaN(euro) || isNaN(kurs)) throw Response.badRequest({ fehler: "euro und kurs müssen Zahlen sein" });
        return { euro: euro, kurs: kurs, kronen: Math.round(euro * kurs * 100) / 100 };
      }
    }
  ]
};

23. Ein Lebenszeichen für die Überwachung

Überwachungs-Werkzeuge fragen regelmäßig ab, ob ein Dienst läuft. Status 503 signalisiert „gerade nicht verfügbar".

module.exports = {
  path: "/beispiel/status",
  operations: [
    {
      id: "status",
      method: "GET",
      handler: function () {
        var dbOk;
        try {
          db.scalar("SELECT 1");
          dbOk = true;
        } catch (e) {
          dbOk = false;
        }
        var antwort = { dienst: "KCForms", datenbank: dbOk ? "OK" : "GESTÖRT", zeit: new Date().toISOString() };
        return dbOk ? antwort : Response.status(503, antwort);
      }
    }
  ]
};