Dynamic Query
Erstellen von komplexen Datenbankabfragen via JSON: Selektion, Filterung, Joins und Aggregationen.
Übersicht
Der Endpunkt /api/v1/dynamic/query ermöglicht es, flexible und komplexe Datenbankabfragen direkt über ein JSON-Objekt zu definieren. Die Logik abstrahiert SQL-Befehle wie SELECT, WHERE, JOIN und GROUP BY, um Daten dynamisch, aber typsicher abzurufen.
Grundstruktur der Anfrage
Ein minimaler Request benötigt das Zielmodell (query_model) und die gewünschten Felder (query_attributes).
curl https://api.livoi.de/api/v1/dynamic/query \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--data '{
"query_model": "user",
"query_attributes": ["id", "email"],
"page": { "page": 1, "size": 10 }
}'Schema & Metadaten
Um valide Abfragen zu konstruieren, bietet die API Endpunkte zur Introspektion (Discovery) an. Hiermit können verfügbare Tabellen und deren Attribute ermittelt werden.
Verfügbare Tabellen auflisten
Gibt eine Liste aller für dynamische Abfragen freigegebenen Modelle / Tabellen zurück. Optional können direkt alle Attribute mitgeliefert werden.
curl 'https://api.livoi.de/api/v1/dynamic/query/tables?include_attributes=true' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'Beispiel-Antwort:
{
"tables": {
"tenant": [
"id",
"tenant_domain",
"..."
],
"user": [
"id",
"username",
"..."
]
}
}Attribute eines Modells abrufen
Ermittelt spezifische Attribute für ein einzelnes Modell, die in query_attributes oder query_filter verwendet werden können.
curl 'https://api.livoi.de/api/v1/dynamic/query/attributes?query_model=user' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'Beispiel-Antwort:
{
"attributes": [
"id",
"username",
"email",
"created_at",
"..."
]
}SQL-Operationen im Detail
Die API unterstützt die wichtigsten SQL-Klauseln. Hier wird erklärt, wie diese im JSON-Format abgebildet werden.
Technische Einschränkung: Keine Sub-Queries
Die Query-Engine unterstützt derzeit keine Nested Sub-Selects oder verschachtelten Unterabfragen.
1. SELECT (Attribute & Aggregationen)
Das Feld query_attributes bestimmt, welche Spalten zurückgegeben werden. Es entspricht dem SELECT-Teil einer SQL-Query.
Wähle Spalten des Hauptmodells oder verknüpfter Modelle (via Punkt-Notation).
{
"query_attributes": [
"id",
"email",
"tenant.name" // Zugriff auf Join-Tabelle
]
}Um Spalten umzubenennen (analog zu SELECT col AS alias), kann eine Liste aus Tupeln [Ausdruck, Alias] übergeben werden.
{
"query_attributes": [
["email", "user_email"],
["tenant.name", "organization"]
]
}Die API akzeptiert eine gemischte Liste aus Strings und Tupeln. Tupel folgen dem Format ['Attribut', 'Alias']. Das Attribut kann dabei entweder ein direkter Spaltenname oder eine SQL-Aggregatfunktion sein.
Verfügbare Funktionen: MAX, MIN, COUNT, SUM, AVG.
{
"query_attributes": [
["COUNT(id)", "total_users"],
["MAX(created_at)", "last_registration"]
],
"group_by": [] // Ggf. notwendig bei Mix mit normalen Spalten
}2. WHERE (Filterung)
Das query_filter-Objekt übersetzt Schlüssel-Wert-Paare in SQL-WHERE-Klauseln.
Das Format folgt dem Schema: Feld__Operator.
Feld-Auflösung
Filter können sich auf das Hauptmodell beziehen (email) oder auf gejointe Tabellen (tenant.name).
Verfügbare Operatoren:
| Suffix | SQL Äquivalent | Beispiel JSON |
|---|---|---|
(kein Suffix) / __eq | = | "status": "active" |
__ne | != | "status__ne": "deleted" |
__lt | < | "age__lt": 18 |
__lte | <= | "amount__lte": 500 |
__gt | > | "age__gt": 18 |
__gte | >= | "created_at__gte": "2024-01-01" |
__like | LIKE (Case-sensitive) | "code__like": "ABC%" |
__ilike | ILIKE (Case-insensitive) | "email__ilike": "%@gmail.com" |
__in | IN (...) | "role__in": ["admin", "editor"] |
__isnull | IS NULL (true) / IS NOT NULL (false) | "deleted_at__isnull": true |
Beispiel:
{
"query_filter": {
"tenant.name__ilike": "Acme%",
"created_at__gte": "2025-01-01",
"status": "active"
}
}Erweiterte Filterlogik (AND / OR, verschachtelte Bedingungen)
Neben einfachen Key-Value-Filtern unterstützt die Query-Engine komplexe logische Ausdrücke.
Damit lassen sich Bedingungen beliebig tief verschachteln und miteinander kombinieren.
Es gibt zwei grundsätzliche Formen:
- Flache Filterobjekte (ohne
and/or) - Explizite Logikblöcke mit
andoderor
Beide Varianten können kombiniert werden.
Standardverhalten (implizites AND)
Wenn du nur einfache Filter ohne and oder or angibst, werden sie automatisch mit AND verknüpft.
Beispiel: Alle User mit ADMIN-Rechten, die ab 2024 erstellt wurden:
{
"query_model": "user",
"query_filter": {
"permission_level": "ADMIN",
"created_at__gte": "2024-01-01T00:00:00Z"
}
}Das entspricht logisch:
{
"query_model": "user",
"query_filter": {
"and": [
{ "permission_level": "ADMIN" },
{ "created_at__gte": "2024-01-01T00:00:00Z" }
]
}
}Merke:
- Flache Struktur ⇒ implizites AND
- Du musst
andnicht explizit setzen, kannst es aber.
Logische Blöcke (and / or)
Wenn du komplexere Ausdrücke brauchst, kannst du explizit and oder or verwenden.
Ein logischer Block nutzt einen der Schlüssel:
and: Alle Bedingungen im Block müssen erfüllt seinor: Mindestens eine Bedingung im Block muss erfüllt sein
Beispiel: Alle User, die Admin sind und seit 2024 existieren – diesmal explizit als Block:
{
"query_model": "user",
"query_filter": {
"and": [
{ "permission_level": "ADMIN" },
{ "created_at__gte": "2024-01-01T00:00:00Z" }
]
}
}Beispiel: Alle User, die entweder eine bestimmte E-Mail-Domain haben oder zu einem bestimmten Tenant gehören.
Dazu wird user mit tenant gejoint (über tenant_id → tenants.id):
{
"query_model": "user",
"joins": [
{ "target_model": "tenant", "on": "tenant_id=id" }
],
"query_filter": {
"or": [
{ "email__ilike": "%@example.com" },
{ "tenant.domain__ilike": "%.acme.com" }
]
}
}Jedes Element der and-/or-Liste kann selbst wiederum ein logischer Block sein.
So entstehen verschachtelte Bäume aus Bedingungen.
Kombination: flache Filter + and/or
Es ist erlaubt, flache Filter und Logikblöcke im selben query_filter zu mischen.
Semantik:
- Alle flachen Felder im
query_filterwerden implizit mit AND verknüpft. - Wenn zusätzlich ein
and- oderor-Block vorkommt, gilt:- flache Filter AND (Logikblock)
Beispiel:
- Tenant-Domain muss zu
*.acme.compassen (flacher Filter, implizites AND) - UND der User muss entweder Admin/Owner sein
- UND zusätzlich (nie eingeloggt ODER sehr alter Login)
{
"query_model": "user",
"joins": [
{ "target_model": "tenant", "on": "tenant_id=id" }
],
"query_filter": {
"tenant.domain__ilike": "%.acme.com",
"and": [
{ "permission_level__in": ["ADMIN", "OWNER"] },
{
"or": [
{ "last_login__isnull": true },
{ "last_login__lt": "2024-01-01T00:00:00Z" }
]
}
]
}
}Logisch entspricht das:
tenant.domain__ilikeAND (permission_levelin (…) AND (last_loginIS NULL OR … ) )
Multi-Depth (Joins + verschachtelte Logik)
„Multi-Depth“ bedeutet hier:
Mehrere Joins + verschachtelte AND/OR-Blöcke, die auf Felder verschiedener Modelle zugreifen.
Im folgenden Beispiel:
- Startmodell:
user - Join auf
tenantübertenant_id=id - Join auf
agentübertenant_id=tenant.id(Agents gehören zu einem Tenant) - Filter:
- Entweder Tenant-Domain + Admin-Level
- oder der Agent-Name enthält „support“
{
"query_model": "user",
"joins": [
{
"target_model": "tenant",
"on": "tenant_id=id"
},
{
"target_model": "agent",
"on": "tenant_id=tenant.id",
"isouter": true
}
],
"query_attributes": [
"id",
"email",
"tenant.domain",
"agent.name"
],
"query_filter": {
"or": [
{
"and": [
{ "tenant.domain__ilike": "%.acme.com" },
{ "permission_level": "ADMIN" }
]
},
{ "agent.name__ilike": "%support%" }
]
}
}3. JOIN (Verknüpfungen)
Es findet keine implizite Auflösung von Relationen statt. Der Zugriff auf Felder anderer Tabellen (z. B. via tenant.name) erfordert, dass das Zielmodell / Zieltabelle zuvor explizit in der joins-Liste registriert wurde.
Target Model
Das Zielmodell / Zieltabelle, das verknüpft werden soll (target_model).
ON Clause (Optional)
Die ON-Bedingung legt fest, wie verknüpft wird (on).
- Automatisch:
Wird die ON-Angabe weggelassen, führt das System den Join automatisch anhand der vorhandenen Datenbank-Relationen (Foreign Keys) zwischen dem Parent-Modell / der Parent-Tabelle und dem Zielmodell aus. - Explizit:
Formatlinkes_feld=rechtes_feld.
Wird kein Tabellenpräfix angegeben, bezieht sich die linke Seite auf das aktuelle Parent-Modell / die Parent-Tabelle.
Join Type
Mit "isouter": true wird ein LEFT OUTER JOIN erzeugt. Standard (false oder weggelassen) ist ein INNER JOIN.
Beispiel:
{
"query_model": "user",
"joins": [
{
"target_model": "tenant",
"on": "tenant_id=id",
"isouter": true
}
],
"query_attributes": ["email", "tenant.name"]
}4. ORDER BY (Sortierung)
Sortiert die Ergebnismenge. Die Reihenfolge im Array bestimmt die Priorität (Primär-, Sekundärsortierung). Dies ist besonders nützlich, wenn das erste Feld viele gleiche Werte enthält (z. B. Status oder Kategorie).
Feldname: Aufsteigend (ASC)-Feldname: Absteigend (DESC)
{
"query_model": "user",
"order_by": [
"permission_level", // 1. Gruppiert nach Rolle (alle ADMINs, dann MEMBER...)
"-last_login" // 2. Innerhalb der Rolle: Wer zuletzt online war (Neueste zuerst)
]
}5. GROUP BY (Gruppierung)
Notwendig, wenn Aggregationsfunktionen verwendet werden, um Ergebnisse zu clustern.
Beispiel: Analyse der Token-Nutzung pro KI-Modell.
{
"query_model": "history",
"query_attributes": [
"model_name",
["COUNT(id)", "message_count"],
["SUM(usage_output_tokens)", "total_generated_tokens"]
],
"group_by": [
"model_name"
],
"order_by": [
"-total_generated_tokens"
]
}Vollständiges Beispiel
Hier eine Abfrage, die Joins, Filter, Projektion über Tabellengrenzen hinweg und Sortierung kombiniert.
Szenario: Zeige alle Benutzer (user), die seit dem 01.01.2024 erstellt wurden. Rufe dazu id, email und den Namen des zugehörigen Tenants (tenant.name) ab. Die Ergebnisse werden absteigend nach Erstellungsdatum sortiert.
curl https://api.livoi.de/api/v1/dynamic/query \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"query_model": "user",
"query_attributes": [
"id",
"email",
"tenant.name"
],
"joins": [
{
"target_model": "tenant",
"on": "tenant_id=id",
"isouter": true
}
],
"query_filter": {
"created_at__gte": "2024-01-01"
},
"order_by": [
"-created_at"
],
"page": {
"page": 1,
"size": 25
}
}'Fehlerbehandlung
Die API validiert Felder gegen die MODEL_REGISTRY sowie die Syntax der Anfrage. Fehler werden in einem standardisierten JSON-Format zurückgegeben.
Tritt auf, wenn dem Benutzer die notwendigen Berechtigungen für die angefragte Ressource oder Aktion fehlen.
{
"status_code": "403",
"status_message": "Forbidden",
"errors": [
{
"info": "Access denied. You do not have permission to perform this action.",
"type": "Exception",
"cause": {
"details": "Access denied. You do not have permission to perform this action."
}
}
]
}Tritt auf, wenn das angefragte query_model, ein Join-Ziel oder eine spezifische Ressource nicht in der Registry gefunden wurde.
{
"status_code": "404",
"status_message": "Not Found",
"errors": [
{
"info": "Requested resource could not be found.",
"type": "Exception",
"cause": {
"details": "Requested resource could not be found."
}
}
]
}Tritt bei Validierungsfehlern auf (z. B. ungültige Attribute, falsche Datentypen oder unauflösbare on-Klauseln).
{
"status_code": "422",
"status_message": "Unprocessable Content",
"errors": [
{
"info": "Validation failed. One or more fields are invalid.",
"type": "ValueError",
"cause": {
"details": "Validation failed. One or more fields are invalid."
}
}
]
}Ein unerwarteter Fehler auf Serverseite.
{
"status_code": "500",
"status_message": "Internal Server Error",
"errors": [
{
"info": "Unexpected internal server error occurred.",
"type": "Exception",
"cause": {
"details": "Unexpected internal server error occurred."
}
}
]
}