Schema und Benennung
Das GraphQL-Schema Ihres Workspace wird aus Ihren Custom-Object-Definitionen erzeugt. Nichts daran ist handgeschrieben: Jeder Typ, jedes Feld und jedes Argument unten folgt mechanisch aus den Definitionen, die Sie über REST angelegt haben. Ändern Sie eine Definition, ändert sich das Schema mit.
Weil es pro Workspace gilt, wird kein statisches Schemadokument veröffentlicht. Laden Sie das aktuelle herunter:
curl "https://api.tourfold.com/api/v2/graphql/schema" \
-H "Authorization: Bearer YOUR_TOKEN"
Die Antwort ist SDL als text/plain und eignet sich für eine GraphQL-IDE, Insomnia, GraphiQL oder ein
Codegen-Werkzeug, das auf eine Datei zeigt. Speichern Sie sie und laden Sie sie nach jeder
Definitionsänderung erneut.
Da das Schema pro Workspace gilt und zur Laufzeit erzeugt wird, ist Codegenerierung zur Buildzeit gegen ein gemeinsames Schema nicht möglich. Generieren Sie gegen Ihre eigene heruntergeladene SDL und wiederholen Sie das, wenn sich Ihre Definitionen ändern.
Wurzelfelder
Jede Custom-Object-Definition steuert genau ein Wurzelfeld bei, benannt nach dem Slug der Definition, das eine Connection zurückgibt:
type Query {
equipment(where: equipmentFilter, order_by: [equipmentOrderBy!], first: Int, after: String, last: Int, before: String, offset: Int): equipmentConnection!
maintenance_request(where: maintenance_requestFilter, ...): maintenance_requestConnection!
}
Der Feldname ist der Singular-Slug — also equipment, nicht equipments —, weil er der
Definitions-Slug unverändert ist und keine Pluralbildung stattfindet.
Ein Workspace ohne Custom-Object-Definitionen erhält ein Platzhalterschema, dessen einziges Feld
_empty: String ist. Das ist kein Fehler, sondern bedeutet, dass es noch nichts abzufragen gibt.
Typ- und Feldnamen
GraphQL-Namen sind auf [_A-Za-z][_0-9A-Za-z]* beschränkt, Tourfold-Eigenschaftsnamen nicht. Namen werden
daher bereinigt: Jedes Zeichen außerhalb dieses Zeichensatzes wird zu _, und einer führenden Ziffer
wird ein _ vorangestellt.
| Definitions-Slug oder Eigenschaftsname | GraphQL-Name |
|---|---|
equipment | equipment |
asset tag | asset_tag |
Power (kW) | Power__kW_ |
2nd_reading | _2nd_reading |
:::warning Zwei Eigenschaftsnamen können kollidieren
Die Bereinigung ist nicht umkehrbar, deshalb können zwei unterschiedliche Eigenschaftsnamen denselben
GraphQL-Feldnamen ergeben: Aus power kw und power-kw wird jeweils power_kw. In diesem Fall erscheint
nur einer von beiden im Schema und der andere ist über GraphQL stillschweigend nicht erreichbar. Es
wird kein Fehler gemeldet — weder beim Anlegen der Definition noch bei der Abfrage; über REST bleibt die
Eigenschaft vorhanden.
Vermeiden Sie Eigenschaftsnamen, die sich nur in Satzzeichen oder Leerzeichen unterscheiden. Fehlt ein erwartetes Feld in der SDL, prüfen Sie dieselbe Definition auf einen nahezu gleichlautenden Namen. :::
Die erzeugten Typnamen folgen festen Mustern und sind damit vorhersagbar:
| Art | Muster | Beispiel |
|---|---|---|
| Objekttyp | <slug> | equipment |
| Connection | <slug>Connection | equipmentConnection |
| Edge | <slug>Edge | equipmentEdge |
| Filter-Input | <slug>Filter | equipmentFilter |
| Order-by-Input | <slug>OrderBy | equipmentOrderBy |
| Filter für verschachteltes Objekt | <slug>_<feld>Filter | equipment_specsFilter |
| Order-by für verschachteltes Objekt | <slug>_<feld>OrderBy | equipment_specsOrderBy |
| Enum | <slug>_<feld> | equipment_category |
Abbildung der Skalartypen
type und format einer Eigenschaft im JSON Schema bestimmen ihren GraphQL-Typ:
| JSON Schema | GraphQL | Hinweise |
|---|---|---|
string | String | |
string + format: uuid | ID | |
string + format: date | Date | 2026-03-14 |
string + format: time | Time | 09:30:00 |
string + format: date-time | DateTime | RFC 3339, z. B. 2026-03-14T09:30:00Z |
number | Float | Gilt auch für Ganzzahlen — eine Abbildung auf Int gibt es nicht. |
boolean | Boolean | |
array | [T] | T aus items.type des Arrays; Standard ist String. |
object | String | JSON-kodiert, siehe unten. |
totalCount ist ein Long, kein Int — eine Sammlung kann mehr als 2³¹ Datensätze enthalten.
Verschachtelte Objekte kommen als JSON-String zurück
Eine Eigenschaft vom Typ object wird als String mit dem JSON-Dokument zurückgegeben, nicht als
GraphQL-Objekttyp:
{ "specs": "{\"manufacturer\":\"Kelvion\",\"power_kw\":42.5}" }
Parsen Sie ihn clientseitig. Beachten Sie die Asymmetrie: In ein verschachteltes Objekt hinein selektieren können Sie nicht, aber Sie können nach seinen inneren Feldern filtern und sortieren (siehe Filtern und Sortieren) — die Filter-Input-Typen werden aus dem verschachtelten Schema erzeugt, auch wenn der Ausgabetyp ein String ist.
Enums
Eine string-Eigenschaft mit nicht-leerer enum-Liste wird zu einem GraphQL-Enum namens
<slug>_<feld>; jeder Wert wird wie ein Feldname bereinigt:
enum equipment_category {
hvac
elevator
lighting
}
Beim Filtern akzeptiert ein Enum-Feld nur _in, und der Operand ist eine Liste von Strings
(_in: ["hvac"]), nicht das nackte Enum-Literal — der Enum-Typ gilt für das Ausgabefeld, nicht für den
Filter-Input.
Pflichtfelder
Eine in der Definition als readOnly markierte Eigenschaft wird als non-null ausgegeben (String!). Alles
andere ist nullable, auch Eigenschaften, die in der Definition als required geführt werden — required
beschränkt Schreibvorgänge, und die führt GraphQL nicht aus.
Systemfelder
Jeder Typ trägt vier Felder, die Tourfold selbst pflegt. Sie deklarieren sie nicht und können sie nicht ändern:
| Feld | Typ | |
|---|---|---|
id | ID! | Die UUID des Datensatzes. |
created_at | DateTime! | |
updated_at | DateTime! | |
lock_version | Float! | Zähler für optimistisches Locking, genutzt beim Schreiben über REST. |
lock_version ist selektierbar, kann aber nicht gefiltert oder sortiert werden — es ist
schreibseitige Buchführung ohne sinnvolle Vergleichssemantik. Die anderen drei filtern und sortieren wie
jedes Feld ihres Typs.
Ein berechnetes Anzeigenamensfeld gibt es nicht. Wenn Sie eines brauchen, selektieren Sie die Eigenschaften, aus denen es sich zusammensetzt.
Relationen
Jede Relation, die Sie definieren, steuert beiden beteiligten Typen ein Feld bei, benannt nach dem Slug dieser Seite in Richtung der anderen. Die Form richtet sich nach der Kardinalität des Ziels:
To-one — ein einfaches, nullable Objektfeld ohne Argumente:
type maintenance_request {
equipment: equipment
}
To-many — eine Connection mit denselben Argumenten für Filtern, Sortieren und Paginieren wie ein Wurzelfeld:
type equipment {
maintenance_requests(
where: maintenance_requestFilter
order_by: [maintenance_requestOrderBy!]
first: Int
after: String
last: Int
before: String
offset: Int
): maintenance_requestConnection
}
Eine Abfrage kann damit in beide Richtungen laufen:
query EquipmentWithOpenRequests {
equipment(first: 5, order_by: [{ name: asc }]) {
edges {
node {
name
maintenance_requests(where: { resolved: { _eq: false } }, first: 3, order_by: [{ reported_at: desc }]) {
totalCount
edges {
node {
title
priority
}
}
}
}
}
}
}
Aus den Erzeugungsregeln folgt einiges unmittelbar:
- Selbstreferenzierende Relationen funktionieren. Eine Definition, die mit sich selbst verknüpft ist, erhält beide Felder auf demselben Typ.
- Verborgene Relationsseiten werden weggelassen. Eine als verborgen markierte Relationsseite steuert kein Feld bei, weder im Ausgabetyp noch im Filter-Input.
- Verschachtelte Connections haben eine eigene, kleinere Standard-Seitengröße — 10 statt 20 —, weil sie einmal pro übergeordnetem Datensatz aufgelöst werden. Siehe Paginierung.
- Die Traversierungstiefe ist begrenzt. Eine Connection kostet mehrere Ebenen gegen das Tiefenlimit, siehe Fehler und Limits.
Wann das Schema aktualisiert wird
Das kompilierte Schema wird pro Workspace zwischengespeichert, mit einem Token als Schlüssel, das sich bei jeder Änderung einer Definition oder Relation ändert. Eine Definitionsänderung ist deshalb bei der nächsten Abfrage sichtbar, auf jeder API-Instanz — Sie müssen kein Cache-Fenster abwarten und treffen nicht sporadisch auf ein veraltetes Schema, je nachdem welche Instanz Sie bedient hat.
Zwischengespeicherte Schemata werden außerdem nach zehn Minuten ohne Änderung verworfen. Beobachtbar ist davon nichts, außer dass die erste Abfrage danach geringfügig langsamer ist.
Typen, die Sie möglicherweise nicht sehen
Custom-Object-Typen können zugriffsbeschränkt sein. Darf Ihre Rolle einen Typ nicht lesen, erscheint er
weiterhin im Schema, aber eine Abfrage liefert eine leere Connection statt eines Fehlers — und eine
To-one-Relation dorthin wird zu null. Der Rest der Abfrage wird weiterhin ausgeführt und liefert weiterhin
Daten.
Das ist Absicht (eine Abfrage soll nicht an einem unzugänglichen Zweig scheitern), bedeutet aber: Ein leeres Ergebnis ist kein Beweis für eine leere Sammlung. Ist ein Typ, den Sie als gefüllt kennen, dauerhaft leer, prüfen Sie zuerst die Zugriffsrechte der Rolle, bevor Sie auf fehlende Datensätze schließen. Typen, die nur als Kind eines anderen Datensatzes existieren, erscheinen nie auf der Wurzelebene, sondern sind ausschließlich über ihren Elterndatensatz erreichbar.