Zum Hauptinhalt springen

Abfragen

Ein HTTP POST auf /data/<ns>/<api>/query durchsucht die Datensätze einer API frei. Der Body hat folgenden Aufbau:

interface IQueryRequest {
/** Einschränkung der Datensätze. */
filter?: unknown
/** Sortierung des Ergebnisses. */
sort?: Record<string, 'ascending' | 'descending'>
/** Nullbasierte Seite. */
pageIndex?: number
/** Höchstzahl Datensätze, 1 bis 10000, Voreinstellung 100. */
pageSize?: number
/** Vergleiche ohne Unterscheidung von Groß- und Kleinschreibung. */
noCase?: boolean
}
  • pageSize ist eine ganze Zahl zwischen 1 und 10.000, ohne Angabe 100.
  • pageIndex blättert auf Basis von pageSize, eine ganze Zahl ab 0, ohne Angabe 0.
  • sort wird vor dem Blättern angewendet.
  • noCase: Filter und Sortierung unterscheiden nicht zwischen Groß- und Kleinschreibung.
curl -X POST -u "$USER" -H 'Content-Type: application/json' \
-d '{ "filter": { "age": { "$gte": 18 } }, "sort": { "name": "ascending" }, "pageSize": 20, "pageIndex": 0 }' \
"$CMS/data/tutorial/people/query"

Mit ?exported liefert die Abfrage die Datensätze für die Darstellung, siehe Datenschnittstelle.

Feldnamen​

Für Sortierung und Filter gelten dieselben Regeln für Feldnamen:

  • Ein direktes Feld wird mit seinem technischen Namen angegeben, z. B. age.
  • Das gilt auch für Listen (array), z. B. hobbies. Eine Sortierung danach ist allerdings wenig aussagekräftig, die Elemente selbst werden nicht sortiert.
  • Unterfelder einer Substruktur werden mit Punkt angegeben, z. B. address.city.
  • Bei lokalisierbaren Feldern wird das Sprachkürzel mit Punkt angehängt, z. B. title.de.
  • Die Regeln lassen sich kombinieren, z. B. images.de.title für die Bildunterschrift title in einer Liste images lokalisierbarer Substrukturen.

Sortiert werden kann nur nach Feldern, deren Meta-Daten sortable setzen. Für Filter gibt es diese Einschränkung nicht, die verfügbaren Vergleiche hängen aber vom Datentyp ab (Tabellen unten).

Felder des CMS​

Zusätzlich stehen die Meta-Daten des CMS zur Verfügung:

FeldArtBedeutung
_idKennungKennung des Datensatzes (nur Filter, z. B. { "_id": { "$in": ["…", "…"] } })
_cms.createdUhrzeitZeitpunkt der Erstellung
_cms.createdByKennungKonto, das den Datensatz angelegt hat
_cms.lastModifiedUhrzeitZeitpunkt der letzten Änderung, anfangs gleich created
_cms.modifiedByKennungKonto, das den Datensatz zuletzt geändert hat

Die Pflegeanwendung sortiert Listen zunächst absteigend nach _cms.lastModified, die zuletzt geänderten Datensätze stehen also oben.

Eine Uhrzeit erlaubt dieselben Vergleiche wie eine Zahl, muss aber in besonderer Form angegeben werden: ISODate("2020-11-03T11:44:59.62512Z"). Der innere Text ist immer eine Angabe nach ISO 8601 in UTC, Sekundenbruchteile sind optional (eine bis fünf Ziffern).

{
"_cms.lastModified": { "$gt": "ISODate(\"2026-10-21T08:00:00Z\")" }
}

Damit sollen sich gezielt die seit einem Zeitpunkt geänderten Datensätze abrufen lassen, etwa für einen Abgleich. Derzeit funktioniert das nicht, siehe Bekannte Einschränkungen. Gelöschte Datensätze erkennt man auf diesem Weg ohnehin nicht, dafür müssen die vorhandenen Kennungen verglichen werden.

Filter​

Im einfachsten Fall wird nach den Werten eines Felds eingeschränkt:

{
"age": { "$exists": true }
}

Das Ergebnis enthält nur Datensätze, deren Feld age einen Wert hat. $exists mit true oder false steht für alle Datentypen zur Verfügung.

Die meisten Datentypen erlauben den Vergleich mit einem oder mehreren Werten:

{
"address.city": { "$in": ["Bonn", "Rheinbreitbach"] },
"age": 57,
"hobbies": { "$nin": ["Oper", "Freeclimbing"] },
"married": { "$ne": true },
"name": { "$eq": "Jochen" }
}

Gefunden werden Datensätze, deren address.city Bonn oder Rheinbreitbach ist ($in), deren age 57 ist, deren name gleich ($eq) Jochen ist, deren married nicht ($ne) true ist und in deren Liste hobbies weder Oper noch Freeclimbing vorkommt ($nin).

Datentypen mit einer Ordnung erlauben Größenvergleiche:

{
"age": { "$gte": 18 }
}

Das liefert Datensätze mit age mindestens ($gte) 18. Weitere Vergleiche: echt größer ($gt), echt kleiner ($lt), höchstens ($lte).

Zeichenketten​

Zeichenketten bieten zusätzlich Mustervergleiche ($regex mit $options) und eine Volltextsuche ($text). Für die Volltextsuche muss fulltext in den Meta-Daten des Felds gesetzt sein.

{
"first": { "$regex": "^J.che[a-z]", "$options": "i" },
"vita": { "$text": { "$search": "React", "$caseSensitive": true } }
}

first wird mit dem regulären Ausdruck ^J.che[a-z] verglichen, ohne Unterscheidung von Groß- und Kleinschreibung ($options). vita muss in der Volltextsuche den Begriff React in exakter Schreibweise enthalten. $text kennt außerdem $language und $diacriticSensitive.

GEO-Koordinaten​

Für GEO-Koordinaten gibt es neben $exists nur $near. Beide lassen sich nicht kombinieren. Angegeben werden ein Bezugspunkt ($geometry, in coordinates wie üblich zuerst der Längengrad) sowie eine maximale ($maxDistance) und minimale ($minDistance) Entfernung in Metern.

{
"location": {
"$near": {
"$geometry": { "coordinates": [9.2150267, 49.1443855], "type": "Point" },
"$maxDistance": 280000,
"$minDistance": 250000
}
}
}

$maxDistance und $minDistance sind optional. $near muss auf oberster Ebene des Filters stehen, nicht innerhalb von $and, $or oder $not.

Ohne sort liefert eine Abfrage mit $near die Datensätze nach Entfernung, die nächsten zuerst. Mit sort gilt stattdessen die angegebene Sortierung.

Verknüpfungen​

Mehrere Bedingungen im Filter werden mit logischem UND verknüpft:

{
"age": 42,
"address.city": "Bonn"
}

Mit $and, $or und $not lassen sich auch komplexe Suchen formulieren:

{
"$and": [
{ "age": 42 },
{ "$not": { "name": "Jochen" } },
{
"$or": [
{ "gender": "male", "address.city": "Bonn" },
{ "gender": "female", "address.city": "Köln" }
]
}
]
}

Gesucht werden alle Datensätze mit age 42, deren name nicht Jochen ist und die entweder gender male und address.city Bonn oder gender female und address.city Köln haben. Das $and könnte hier auch entfallen, da die Bedingungen eines Filters ohnehin mit UND verknüpft werden.

Vergleiche je Datentyp​

Datentyp$existsWert$eq $ne $in $nin$gt $gte $lt $lte
booleanjajajanein
emailjajajaja
filejaneinneinnein
htmljaneinneinnein
numberjajajaja
stringjajajaja
geo-pointjaneinneinnein
Uhrzeitjajajaja
Kennungjajajaja¹

¹ technisch möglich, für Kennungen aber selten sinnvoll.

Datentyp$regex $options $text$near
emailjanein
stringjanein
geo-pointneinja

Antwort​

Eine Abfrage liefert neben dem gewünschten Ausschnitt weitere Angaben:

interface IQueryResponse<T = unknown> {
/** Anzahl der Datensätze, die zum Filter passen. */
filterCount: number
/** Datensätze im angeforderten Ausschnitt. */
items: T[]
/** Anzahl aller Datensätze der API. */
totalCount: number
/** Tatsächlich verwendete Seitengröße. */
pageSize: number
/** Tatsächlich verwendete Seite. */
pageIndex: number
}

filterCount ist mindestens so groß wie die Anzahl in items, totalCount mindestens so groß wie filterCount. pageSize und pageIndex sind die tatsächlich verwendeten Werte, auch wenn sie in der Anfrage fehlten.

Bekannte Einschränkungen​

Die folgenden Fehler betreffen CMS-Version 11.0.0 und früher. Im Quelltext sind sie behoben, wirksam werden die Korrekturen mit der nächsten Version. Gleiches gilt für $near: In 11.0.0 sortiert es nicht nach Entfernung, scheitert ohne $minDistance und scheitert bei APIs mit mehr als einer GEO-Koordinate.

  • Filter auf Uhrzeiten liefern keine Treffer. Der Wert ISODate("…") wird geprüft, aber nicht in einen Zeitpunkt umgewandelt. Der Vergleich mit dem gespeicherten Zeitpunkt schlägt daher immer fehl, egal ob mit $gt, $lt oder $eq. $exists und die Sortierung nach _cms.created bzw. _cms.lastModified funktionieren. Workaround für „seit Zeitpunkt X geändert“: absteigend nach _cms.lastModified sortieren und blättern, bis ein älterer Datensatz erscheint.

    { "sort": { "_cms.lastModified": "descending" }, "pageSize": 100 }
  • noCase und filterCount: Mit "noCase": true enthält items die erwarteten Treffer, filterCount wird aber mit Unterscheidung von Groß- und Kleinschreibung gezählt und kann deshalb zu klein sein (bis 0). Für das Blättern mit noCase ist filterCount daher nicht verlässlich.

Alle Datensätze lesen​

Um alle Datensätze zu lesen, blättert man, bis (pageIndex + 1) * pageSize >= filterCount:

async function queryAll(url, filter) {
const all = []
for (let pageIndex = 0; ; pageIndex++) {
const page = await call(`${url}/query`, { method: 'POST', body: JSON.stringify({ filter, pageIndex, pageSize: 1000 }) })
all.push(...page.items)
if ((page.pageIndex + 1) * page.pageSize >= page.filterCount) return all
}
}