{ }
type
GraphQL · Filter · Sortierung · Pagination · Input Types
Filter und Sortierung
in GraphQL APIs modellieren

Wie man Filter- und Sortierargumente als saubere Input-Typen modelliert, Operator-Muster für verschiedene Datentypen implementiert, Magento's Filter-Konventionen nutzt und Pagination-Strategien für den jeweiligen Use Case wählt.

14 Min. Lesezeit Input Types · Operatoren · Cursor Pagination · Magento Filter · Sortierung GraphQL · Magento 2.4 · API Design

1. Warum Filter-Design unterschätzt wird

Filter- und Sortierlogik in GraphQL-APIs wirkt auf den ersten Blick trivial: Ein paar Argumente, ein bisschen SQL im Resolver und fertig. In der Praxis ist schlechtes Filter-Design aber einer der häufigsten Gründe für APIs, die schwer erweiterbar sind, Performance-Probleme erzeugen und Frontend-Teams frustrieren. Eine Query, die Filterbedingungen als flache Strings übergibt, ist schwer zu validieren, schwer zu dokumentieren und unmöglich maschinell zu verarbeiten.

Das Gegenstück dazu ist typisiertes Filter-Design mit strukturierten Input-Typen und Operator-Mustern. Dieser Ansatz ist explizit, maschinenlesbar, validierbar und erweiterbar. Magento macht es vor: FilterEqualTypeInput, FilterRangeTypeInput und FilterMatchTypeInput sind typisierte Filter-Input-Typen, die konsistent über alle Produktlisten-Queries verwendet werden. Wer eigene APIs baut, profitiert davon, dieses Muster zu übernehmen – angepasst an den eigenen Datenkontext.

2. Input Types als Basis für Filterlogik

Der erste Schritt zu sauberem Filter-Design ist die Wahl von Input Types statt Scalar-Argumenten. Statt query products(priceMin: Float, priceMax: Float) modelliert man query products(filter: ProductFilterInput), wobei ProductFilterInput ein Input-Typ mit Feldern für verschiedene Filterkriterien ist. Dieser Ansatz ist flexibler – neue Filterfelder können hinzugefügt werden, ohne die Query-Signatur zu ändern – und klarer in der Dokumentation, weil jedes Filterfeld einen expliziten Namen und Typ hat.

Input Types in GraphQL folgen denselben Typ-Regeln wie Output Types, aber sie sind nur als Eingabe erlaubt. Sie können andere Input Types als Felder enthalten, aber keine Output Types. Das erlaubt die Komposition von Filtern: Ein ProductFilterInput kann ein Feld price: PriceRangeFilterInput enthalten, das wiederum from: Float und to: Float definiert. Diese Verschachtelung ist klar, typsicher und semantisch eindeutig.


# Typed filter input types — composable and self-documenting
input PriceRangeFilterInput {
    from: Float
    to: Float
}

input StringFilterInput {
    eq: String         # exact match
    in: [String!]      # match any in list
    like: String       # pattern match (% wildcard)
    neq: String        # not equal
}

input IntFilterInput {
    eq: Int
    gt: Int            # greater than
    lt: Int            # less than
    gte: Int           # greater than or equal
    lte: Int           # less than or equal
    in: [Int!]
}

input ProductFilterInput {
    sku: StringFilterInput
    name: StringFilterInput
    price: PriceRangeFilterInput
    category_id: IntFilterInput
    status: IntFilterInput
}

extend type Query {
    filteredProducts(
        filter: ProductFilterInput
        sort: ProductSortInput
        pageSize: Int = 20
        currentPage: Int = 1
    ): ProductSearchResult
        @resolver(class: "Vendor\\Catalog\\Model\\Resolver\\FilteredProducts")
}

3. Operator-Muster: eq, in, range, like

Operator-Muster strukturieren die Filterbedingungen nach ihrer Semantik. Der einfachste Operator ist eq (exact match): Ein Feld muss exakt einem Wert entsprechen. Für Listenfilterung ist in der passende Operator: Das Feld muss einem der Werte in der Liste entsprechen. Für numerische Bereiche wie Preise oder Datumsangaben eignen sich from/to oder gte/lte-Paare. Für Textsuche ist like mit Wildcard-Unterstützung der Standard.

Ein wichtiger Aspekt von Operator-Mustern ist die Behandlung von null-Werten. In Magento werden nicht angegebene Filter-Felder einfach ignoriert – kein Filter bedeutet keine Einschränkung. Das macht Sinn für optionale Filterfelder, ist aber bei Pflichtfiltern (etwa einem Mandanten-Filter für Multi-Store-Setups) eine Sicherheitslücke. Pflichtfilter sollten als Non-nullable-Argumente deklariert werden, nicht als optionale Felder in einem Input Type – oder der Resolver muss explizit prüfen, ob der Pflichtfilter gesetzt ist.

4. Magento FilterEqualTypeInput: Konventionen nutzen

Magento definiert in Magento_GraphQl/etc/schema.graphqls einige wiederverwendbare Filter-Input-Typen: FilterEqualTypeInput (mit eq und in), FilterRangeTypeInput (mit from und to) und FilterMatchTypeInput (mit match für Volltextsuche). Diese Typen werden in Magentos Produktfiltern verwendet und sind der Standard für eigene Filter in Magento-Modulen, die konsistent mit dem Core-Schema sein sollen.


# Using Magento's built-in filter types for consistency
# These types are defined in Magento_GraphQl/etc/schema.graphqls

input CustomProductFilterInput {
    # Exact match or list: use Magento's FilterEqualTypeInput
    category_uid: FilterEqualTypeInput
    color: FilterEqualTypeInput
    size: FilterEqualTypeInput

    # Numeric range: use Magento's FilterRangeTypeInput
    price: FilterRangeTypeInput
    stock_quantity: FilterRangeTypeInput

    # Full-text match: use Magento's FilterMatchTypeInput
    name: FilterMatchTypeInput
    description: FilterMatchTypeInput
}

# Example query using the filter
query FilteredProductList {
  filteredProducts(
    filter: {
      category_uid: { eq: "MTI=" }
      price: { from: "10.00", to: "100.00" }
      color: { in: ["red", "blue"] }
      name: { match: "running" }
    }
    sort: { name: ASC }
    pageSize: 20
    currentPage: 1
  ) {
    total_count
    items {
      sku
      name
      price_range { minimum_price { final_price { value currency } } }
    }
  }
}

5. Sortierung modellieren: Enum-Felder und Richtung

Sortierlogik in GraphQL wird am saubersten mit einem dedizierten Sort-Input-Typ modelliert. Die Sortierfelder sind Enums – das verhindert fehlerhafte Feldnamen und macht die erlaubten Sortieroptionen für den Client sichtbar. Die Sortierrichtung wird durch einen separaten Enum-Typ SortEnum mit den Werten ASC und DESC ausgedrückt. Magento verwendet dieses Muster in seiner Produkt-API und es ist gut auf andere Ressourcentypen übertragbar.

Eine häufige Designfrage ist, ob Multi-Column-Sorting – Sortierung nach mehreren Feldern gleichzeitig – unterstützt werden soll. Magento unterstützt es: Der Sort-Input-Typ kann mehrere Felder gesetzt haben, und der Resolver interpretiert sie als priorisierte Sortierreihenfolge. Wer Multi-Column-Sorting nicht braucht, sollte trotzdem den Typ so designen, dass er später erweiterbar ist – also einen Input-Typ statt eines Scalar-Arguments für die Sortierung verwenden.

6. Pagination: Offset vs. Cursor-basiert

Pagination in GraphQL ist ein eigenes Designthema. Die einfachste Form ist Offset-Pagination mit pageSize und currentPage: verständlich, weit verbreitet in Magento, aber mit einem bekannten Problem bei Live-Daten. Wenn zwischen zwei Seitenaufrufen ein Datensatz hinzugefügt oder gelöscht wird, verrutscht das Offset und der Client sieht entweder doppelte oder übersprungene Einträge. Für Produktlisten mit seltenen Änderungen ist das akzeptabel; für Activity-Feeds oder Echtzeit-Listen ist es problematisch.

Cursor-basierte Pagination ist das robustere Muster, das im Relay-Specification definiert ist: Der Server gibt für jeden Datensatz einen opaken Cursor mit, und der Client kann diesen Cursor als Startpunkt für die nächste Seite übergeben. Das ist stabil gegenüber Einfügungen und Löschungen während der Paginierung. Die Implementierung ist aufwändiger – der Cursor muss die Sortierposition kodieren und beim nächsten Request decodiert werden – aber das Ergebnis ist eine konsistente Pagination unabhängig von Datenänderungen.


# Offset-based pagination (simple, Magento-native)
type ProductSearchResult {
    items: [Product!]!
    total_count: Int!
    page_info: SearchResultPageInfo!
}

type SearchResultPageInfo {
    page_size: Int!
    current_page: Int!
    total_pages: Int!
}

# Cursor-based pagination (Relay spec — more robust for live data)
type ProductConnection {
    edges: [ProductEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
}

type ProductEdge {
    node: Product!
    cursor: String!   # opaque cursor — do not parse client-side
}

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

# Query using cursor pagination
extend type Query {
    productsFeed(
        first: Int = 20
        after: String   # cursor from previous page's endCursor
        filter: ProductFilterInput
    ): ProductConnection
}

7. Filter an den Resolver und das Repository anbinden

Der Resolver nimmt den strukturierten Filter-Input aus $args['filter'] und muss ihn in Datenbankabfragen übersetzen. In Magento ist das der SearchCriteriaBuilder: Jedes Filterfeld wird in einen FilterGroup-Eintrag übersetzt. Die Zuordnung von GraphQL-Filterfeldern zu Magento-Attributcodes ist eine eigene Aufgabe – sie sollte in einer dedizierten Mapper-Klasse stattfinden, nicht direkt im Resolver. Dieser Mapper ist auch der Ort, an dem unerlaubte Filterfelder explizit ausgeschlossen werden.

Performance ist bei gefilterten Listen kritisch: Ein Filter auf ein nicht-indiziertes Attribut führt zu einem Full-Table-Scan. Der Resolver kann das nicht verhindern, aber er kann es verweigern, indem er nicht-indexierte Filterfelder mit einer GraphQlInputException ablehnt. Eine Whitelist der erlaubten Filterfelder – analog zur Whitelist der erlaubten Sortierfelder – ist das richtige Muster hier. Alles, was nicht explizit erlaubt ist, wird abgelehnt.

8. Sicherheit: was nicht gefiltert werden darf

Filter sind ein Angriffspunkt für Datenexposition: Ein Client könnte versuchen, nach internen Feldern zu filtern, die zwar im Datenbankschema existieren, aber nicht im GraphQL-Schema exponiert sind. Das ist in SQL-basierten Backends besonders relevant, wenn der Resolver Filter direkt in SQL-Bedingungen übersetzt. Eine Whitelist der erlaubten Filterfelder verhindert, dass ein Client nach Feldern wie password_hash, fraud_score oder anderen internen Attributen filtern kann.

Ein zweiter Sicherheitsaspekt ist die Komplexität von Filter-Expressions. Ein Client könnte einen Filter mit hunderten von in-Werten übergeben, der eine aufwändige SQL-IN-Klausel erzeugt. Eine maximale Anzahl von Werten im in-Operator – beispielsweise 100 – verhindert absichtliche oder unabsichtliche Query-Überlastung. Diese Grenze sollte im Resolver oder im Mapper explizit geprüft und bei Überschreitung mit einer GraphQlInputException abgelehnt werden.

9. Zusammenfassung

Filter und Sortierung in GraphQL-APIs sauber zu modellieren bedeutet: typisierte Input Types statt flache Scalar-Argumente, Operator-Muster statt implizite Filterlogik, Whitelist statt freie Feldauswahl, und die richtige Pagination-Strategie für den Use Case. Magento's Filter-Input-Typen sind ein gutes Referenzmodell für eigene Filter in Magento-Modulen und für externe APIs, die ähnliche Filterkonzepte brauchen.

Der Aufwand für sauberes Filter-Design zahlt sich doppelt aus: Das Frontend-Team hat eine klare, dokumentierte API, die keine Implementierungsdetails des Backends kennenlernen muss. Das Backend-Team kann Performance-Optimierungen am Repository-Layer durchführen, ohne die API-Oberfläche zu ändern. Und Sicherheitseigenschaften – Whitelist, Komplexitätsgrenzen – sind explizit und testbar, statt implizit und zufällig.

Filter und Sortierung in GraphQL APIs — Das Wichtigste auf einen Blick

Input Types

Typisierte Filter-Input-Typen statt Scalar-Argumente. Komposition durch verschachtelte Input Types. Magento FilterEqualTypeInput als Vorlage nutzen.

Operatoren

eq, in, range, like als dedizierte Felder im Filter-Input-Typ. Nicht angegebene Filter werden ignoriert. Pflichtfilter als Non-nullable-Argumente.

Pagination

Offset (pageSize/currentPage) für einfache Listen. Cursor-basiert (Relay-Spec) für Live-Daten und konsistente Navigation ohne Duplikate.

Sicherheit

Whitelist der erlaubten Filterfelder im Mapper. Maximale in-Operator-Größe begrenzen. Nicht-indexierte Felder explizit ablehnen.

10. Vergleich: Filter-Modellierungsansätze

Ansatz Beispiel Vorteil Nachteil
Scalar-Argumente products(sku: "ABC") Einfach Nicht erweiterbar, kein Operator
Input Type mit Operator filter: { sku: { eq: "ABC" } } Typsicher, erweiterbar Mehr Schema-Code
Magento FilterEqualTypeInput { eq: "x", in: ["x","y"] } Core-kompatibel, wiederverwendbar Magento-spezifisch
Offset Pagination pageSize: 20, currentPage: 2 Einfach, verständlich Inkonsistent bei Live-Daten
Cursor Pagination first: 20, after: "cursor" Stabil, Relay-kompatibel Komplexere Implementierung

11. FAQ: Filter und Sortierung in GraphQL APIs modellieren

1Warum Input Types statt Scalar-Argumente?
Input Types sind typsicher, selbstdokumentierend und erweiterbar. Neue Filteroptionen ohne Änderung der Query-Signatur hinzufügen. Scalar-Argumente skalieren nicht.
2Was ist Magento's FilterEqualTypeInput?
Eingebauter Magento-Input-Typ mit eq und in. Standard für eigene Filter in Magento-Modulen. Konsistent mit der Core-Produkt-API.
3Cursor- statt Offset-Pagination?
Bei Live-Daten, die sich während der Pagination ändern können. Für statische Produktlisten ist Offset ausreichend und einfacher zu implementieren.
4Filter auf interne Felder verhindern?
Whitelist der erlaubten Filterfelder im Mapper implementieren. Nicht in der Liste: GraphQlInputException vor dem Datenbankaufruf.
5GraphQL-Filter in Magento SearchCriteria übersetzen?
In einer dedizierten Mapper-Klasse, nicht im Resolver. Der Mapper erzeugt FilterGroup-Objekte für den SearchCriteriaBuilder und ist ohne GraphQL-Kontext testbar.
6Filter auf nicht-indiziertes Attribut?
Führt zu Full-Table-Scan und schlechter Performance. Nicht-indexierte Felder explizit ablehnen oder Datenbank-Index für alle erlaubten Filterfelder sicherstellen.
7Mehrere Sortierfelder gleichzeitig?
Ja. Sort-Input-Typ mit mehreren optionalen Feldern. Resolver interpretiert alle gesetzten Felder als priorisierte Reihenfolge. Magento unterstützt das nativ.
8Pflichtfilter modellieren?
Als Non-nullable Query-Argument, nicht als optionales Feld im Filter-Input-Typ. Beispiel: products(storeId: ID!, filter: ...) — storeId kann nicht weggelassen werden.
9Maximale Werte im in-Operator?
Faustregel: 100–200. Darüber Performance-Probleme durch große SQL-IN-Klauseln. Grenze konfigurierbar machen, nicht hartkodieren.
10Filter-Input-Typen für Frontend-Teams dokumentieren?
@doc-Direktiven auf allen Input-Feldern. GraphiQL und Apollo Studio zeigen diese Doku direkt aus der Introspection. Beispielqueries in einem Schema-Registry hinterlegen.