docs: improve schema extensibility documentation and samples

- Added TSDoc for 'createHierarchicalValueFieldResolver'.

- Updated README with 'Extending the Schema' guide and Zabbix preconditions.

- Migrated all MQTT items to Agent 2 'mqtt.get' format across documentation and test data.

- Added 'docs/sample_import_distance_tracker_template.graphql' as a schema extension example.

- Verified all 38 tests pass.
This commit is contained in:
Andreas Hilbig 2026-01-26 17:49:31 +01:00
parent 825cb4d918
commit bff9ee6d2e
5 changed files with 245 additions and 38 deletions

View file

@ -23,6 +23,7 @@ Compared to the original Zabbix API, this GraphQL API provides several key enhan
* On-the-fly permission checks (`hasPermissions`, `userPermissions`). * On-the-fly permission checks (`hasPermissions`, `userPermissions`).
* **Improved Error Reporting**: Detailed error data from Zabbix is appended to GraphQL error messages, making debugging significantly easier. * **Improved Error Reporting**: Detailed error data from Zabbix is appended to GraphQL error messages, making debugging significantly easier.
* **Strongly Typed Schema**: Leverages GraphQL's type system for clear API documentation and client-side code generation. * **Strongly Typed Schema**: Leverages GraphQL's type system for clear API documentation and client-side code generation.
* **Dynamic Schema Extensibility**: Easily extend the API with custom schema snippets and dynamic resolvers for specialized device types without modifying the core code.
## How to Install and Start ## How to Install and Start
@ -71,6 +72,70 @@ npm run prod
The API will be available at `http://localhost:4000/`. The API will be available at `http://localhost:4000/`.
## Extending the Schema
The Zabbix GraphQL API is designed to be highly extensible. You can add your own GraphQL schema snippets and have resolvers dynamically created for them.
### Dynamic Resolvers with `createHierarchicalValueFieldResolver`
The function `createHierarchicalValueFieldResolver` (found in `src/api/resolver_helpers.ts`) allows for the automatic creation of resolvers that map Zabbix items or tags to a hierarchical GraphQL structure. It uses field names and Zabbix item keys (dot-separated) to automatically resolve nested objects.
### Zabbix Preconditions for Hierarchical Mapping
In order for the dynamic resolvers to correctly map Zabbix data to your GraphQL schema, the following preconditions must be met in your Zabbix templates:
* **Key Naming**: Zabbix item keys (or tags) must match the GraphQL field names.
* **Dot Separation**: Use a dot (`.`) as a separator to represent nested object structures. For example, a Zabbix item with the key `state.current.values.temperature` will be automatically mapped to the `temperature` field within the nested structure: `state` -> `current` -> `values` -> `temperature`.
* **Type Hinting**: You can guide the type conversion by prepending a type hint and an underscore to the last token of the key:
* `json_`: Parses the value as a JSON object (useful for complex types).
* `str_`: Forces the value to be treated as a string.
* `bool_`: Forces the value to be treated as a boolean.
* `float_`: Forces the value to be treated as a number.
For a complete example of a Zabbix template designed for schema extension, see the [Distance Tracker Import Sample](docs/sample_import_distance_tracker_template.graphql).
### No-Code Extension via Environment Variables
You can extend the schema and add resolvers without writing any TypeScript code by using the following environment variables:
* **`ADDITIONAL_SCHEMAS`**: A comma-separated list of paths to additional `.graphql` files.
* **`ADDITIONAL_RESOLVERS`**: A comma-separated list of GraphQL Type names for which dynamic hierarchical resolvers should be created.
#### Example
Suppose you have custom device definitions in `schema/extensions/`. You can load them and enable dynamic resolution by setting:
```bash
ADDITIONAL_SCHEMAS=./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql,./schema/extensions/location_tracker_commons.graphql
ADDITIONAL_RESOLVERS=SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice
```
The API will:
1. Load all provided schema files.
2. For each type listed in `ADDITIONAL_RESOLVERS`, it will automatically create a resolver that maps Zabbix items (e.g., an item with key `state.current.values.temperature`) to the corresponding GraphQL fields.
## Sample Environment File
Below is a complete example of a `.env` file showing all available configuration options:
```env
# Zabbix Connection
ZABBIX_BASE_URL=http://your-zabbix-instance/zabbix
ZABBIX_AUTH_TOKEN=your-super-admin-token-here
# General Configuration
ZABBIX_EDGE_DEVICE_BASE_GROUP=Baustellen-Devices
API_VERSION=1.0.0
SCHEMA_PATH=./schema/
# Schema Extensions (No-Code)
ADDITIONAL_SCHEMAS=./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql,./schema/extensions/location_tracker_commons.graphql
ADDITIONAL_RESOLVERS=SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice
# Logging
# LOG_LEVEL=debug
```
## Usage Samples ## Usage Samples
The `docs` directory contains several sample GraphQL queries and mutations to help you get started: The `docs` directory contains several sample GraphQL queries and mutations to help you get started:
@ -81,6 +146,7 @@ The `docs` directory contains several sample GraphQL queries and mutations to he
* **Templates**: * **Templates**:
* [Query Templates](docs/sample_templates_query.graphql) * [Query Templates](docs/sample_templates_query.graphql)
* [Import Templates](docs/sample_import_templates_mutation.graphql) * [Import Templates](docs/sample_import_templates_mutation.graphql)
* [Import Distance Tracker Template](docs/sample_import_distance_tracker_template.graphql) (Schema Extension Example)
* [Delete Templates](docs/sample_delete_templates_mutation.graphql) * [Delete Templates](docs/sample_delete_templates_mutation.graphql)
* **Template Groups**: * **Template Groups**:
* [Import Template Groups](docs/sample_import_template_groups_mutation.graphql) * [Import Template Groups](docs/sample_import_template_groups_mutation.graphql)

View file

@ -0,0 +1,125 @@
### Mutation
Use this mutation to import a template specifically designed to work with the `DistanceTrackerDevice` type provided in the `location_tracker_devices.graphql` schema extension.
This template uses Zabbix Agent 2 MQTT keys (`mqtt.get`) to subscribe to a broker and retrieve device data, which is then parsed into a hierarchical structure compatible with the GraphQL API's dynamic resolvers.
```graphql
mutation ImportDistanceTrackerTemplate($templates: [CreateTemplate!]!) {
importTemplates(templates: $templates) {
host
templateid
message
error {
message
code
data
}
}
}
```
### Variables
The following sample defines the `DISTANCE_TRACKER` template. Note the `deviceType` tag set to `DistanceTrackerDevice`, which instructs the GraphQL API to resolve this host using the specialized `DistanceTrackerDevice` type.
The item keys use the `json_` prefix where appropriate (e.g., `state.current.json_distances`) to ensure that the JSON strings received from Zabbix are automatically parsed into objects/arrays by the GraphQL resolver.
```json
{
"templates": [
{
"host": "DISTANCE_TRACKER",
"name": "Distance Tracker Device Template",
"groupNames": ["Templates/Roadwork/Devices"],
"templates": [
{ "name": "ROADWORK_DEVICE" }
],
"tags": [
{ "tag": "class", "value": "roadwork" },
{ "tag": "deviceType", "value": "DistanceTrackerDevice" }
],
"items": [
{
"name": "MQTT Master Data",
"type": 0,
"key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"device/distance_tracker/data\"]",
"value_type": 4,
"history": "0",
"description": "Master item receiving full JSON payload via MQTT Agent 2"
},
{
"name": "Device Count",
"type": 18,
"key": "state.current.count",
"value_type": 3,
"history": "7d",
"master_item": {
"key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"device/distance_tracker/data\"]"
},
"preprocessing": [
{
"type": 12,
"params": ["$.count"]
}
]
},
{
"name": "Time From",
"type": 18,
"key": "state.current.timeFrom",
"value_type": 4,
"history": "7d",
"master_item": {
"key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"device/distance_tracker/data\"]"
},
"preprocessing": [
{
"type": 12,
"params": ["$.timeFrom"]
}
]
},
{
"name": "Time Until",
"type": 18,
"key": "state.current.timeUntil",
"value_type": 4,
"history": "7d",
"master_item": {
"key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"device/distance_tracker/data\"]"
},
"preprocessing": [
{
"type": 12,
"params": ["$.timeUntil"]
}
]
},
{
"name": "Nearby Distances",
"type": 18,
"key": "state.current.json_distances",
"value_type": 4,
"history": "7d",
"master_item": {
"key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"device/distance_tracker/data\"]"
},
"preprocessing": [
{
"type": 12,
"params": ["$.distances"]
}
]
}
]
}
]
}
```
### Technical Note: Hierarchical Mapping
The `DistanceTrackerDevice` type in the schema extension is mapped using `createHierarchicalValueFieldResolver`. This resolver expects Zabbix items to follow a naming convention that mirrors the GraphQL structure:
- `state.current.count` maps to `state { current { count } }`
- `state.current.json_distances` maps to `state { current { distances } }` (with automatic JSON parsing due to the `json_` prefix)
The `mqtt.get` keys replace the older `mqtt.trap` style, leveraging the Zabbix Agent 2 MQTT plugin for active topic subscriptions.

View file

@ -56,14 +56,14 @@ This sample data is based on the `BT_DEVICE_TRACKER` template from `src/testdata
} }
], ],
"master_item": { "master_item": {
"key": "mqtt.trap[deviceValue/location]" "key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"deviceValue/location\"]"
} }
}, },
{ {
"uuid": "380c4a7d752848cba3b5a59a0f9b13c0", "uuid": "380c4a7d752848cba3b5a59a0f9b13c0",
"name": "MQTT_LOCATION", "name": "MQTT_LOCATION",
"type": 2, "type": 0,
"key": "mqtt.trap[deviceValue/location]", "key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"deviceValue/location\"]",
"value_type": 4, "value_type": 4,
"history": "0" "history": "0"
} }
@ -77,6 +77,7 @@ This sample data is based on the `BT_DEVICE_TRACKER` template from `src/testdata
When converting from Zabbix YAML/XML exports, use the following numeric mappings for items and preprocessing: When converting from Zabbix YAML/XML exports, use the following numeric mappings for items and preprocessing:
#### Item Type (`type`) #### Item Type (`type`)
- `0`: ZABBIX_AGENT
- `2`: ZABBIX_TRAP (TRAP) - `2`: ZABBIX_TRAP (TRAP)
- `18`: DEPENDANT_ITEM (DEPENDENT) - `18`: DEPENDANT_ITEM (DEPENDENT)
- `21`: SIMULATOR_JAVASCRIPT (JAVASCRIPT) - `21`: SIMULATOR_JAVASCRIPT (JAVASCRIPT)

View file

@ -16,6 +16,21 @@ function defaultKeyMappingFunction(key: string): string {
return words.join("") return words.join("")
} }
/**
* Creates a resolver for an object type where each field is resolved by mapping Zabbix items or tags
* to a hierarchical structure based on field names or Zabbix keys.
*
* This function iterates over all fields of the specified GraphQL type and assigns a resolver
* that uses the provided `sourceFieldMapper` to retrieve the value. It automatically detects
* whether a field is a scalar or an object type to guide the mapping process.
*
* @param schema - The executable GraphQL schema.
* @param typename - The name of the GraphQL type for which to create the resolver.
* @param sourceFieldMapper - A function that maps a field name to its value from the parent object.
* It receives the field name, the parent object, and a boolean indicating
* if the requested field is an object type.
* @returns A resolver object containing field-to-function mappings for the specified type.
*/
export function createHierarchicalValueFieldResolver( export function createHierarchicalValueFieldResolver(
schema: any, typename: string, schema: any, typename: string,
sourceFieldMapper: (fieldname: string, parent: any, objectTypeRequested: boolean) => { [p: string]: any } | null): { sourceFieldMapper: (fieldname: string, parent: any, objectTypeRequested: boolean) => { [p: string]: any } | null): {

View file

@ -52,11 +52,11 @@ zabbix_export:
- filtered - filtered
error_handler: DISCARD_VALUE error_handler: DISCARD_VALUE
master_item: master_item:
key: 'mqtt.trap[deviceValue/location]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/location"]'
- uuid: 380c4a7d752848cba3b5a59a0f9b13c0 - uuid: 380c4a7d752848cba3b5a59a0f9b13c0
name: MQTT_LOCATION name: MQTT_LOCATION
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[deviceValue/location]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/location"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 29faf53c033840c0b1405f8240e30312 - uuid: 29faf53c033840c0b1405f8240e30312
@ -132,27 +132,27 @@ zabbix_export:
"timeUntil": v.timeUntil "timeUntil": v.timeUntil
}); });
master_item: master_item:
key: 'mqtt.trap[deviceValue/count]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]'
- uuid: 905c5f1b6e524bd2b227769a59f4df1b - uuid: 905c5f1b6e524bd2b227769a59f4df1b
name: MQTT_COUNT name: MQTT_COUNT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[deviceValue/count]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 6fa441872c3140f4adecf39956245603 - uuid: 6fa441872c3140f4adecf39956245603
name: MQTT_DISTANCE name: MQTT_DISTANCE
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[deviceValue/distance]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/distance"]'
value_type: TEXT value_type: TEXT
- uuid: 69d2afa4a0324d818150e9473c3264f3 - uuid: 69d2afa4a0324d818150e9473c3264f3
name: MQTT_NAME name: MQTT_NAME
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[deviceValue/name]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/name"]'
value_type: TEXT value_type: TEXT
- uuid: 45ff9430d27f47a492c98fce03fc7962 - uuid: 45ff9430d27f47a492c98fce03fc7962
name: MQTT_SERVICE_DATA name: MQTT_SERVICE_DATA
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[deviceValue/ServiceData]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/ServiceData"]'
value_type: TEXT value_type: TEXT
- uuid: 3bf0d3017ea54e1da2a764c3f96bf97e - uuid: 3bf0d3017ea54e1da2a764c3f96bf97e
name: count name: count
@ -164,7 +164,7 @@ zabbix_export:
parameters: parameters:
- $.count - $.count
master_item: master_item:
key: 'mqtt.trap[deviceValue/count]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]'
- uuid: f0d1fc72e2154613b349be86c6bdcfd6 - uuid: f0d1fc72e2154613b349be86c6bdcfd6
name: timeFrom name: timeFrom
type: DEPENDENT type: DEPENDENT
@ -179,7 +179,7 @@ zabbix_export:
- 'T(\d\d:\d\d:\d\d):' - 'T(\d\d:\d\d:\d\d):'
- \1 - \1
master_item: master_item:
key: 'mqtt.trap[deviceValue/count]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]'
- uuid: e55bf604808f4eb4a964ebeefdd9eb9e - uuid: e55bf604808f4eb4a964ebeefdd9eb9e
name: timeUntil name: timeUntil
type: DEPENDENT type: DEPENDENT
@ -194,7 +194,7 @@ zabbix_export:
- 'T(\d\d:\d\d:\d\d):' - 'T(\d\d:\d\d:\d\d):'
- \1 - \1
master_item: master_item:
key: 'mqtt.trap[deviceValue/count]' key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]'
tags: tags:
- tag: class - tag: class
value: roadwork value: roadwork
@ -236,8 +236,8 @@ zabbix_export:
items: items:
- uuid: 4ad4d9a769744615816d190c34cb49c7 - uuid: 4ad4d9a769744615816d190c34cb49c7
name: GPS_LOCATION_MQTT name: GPS_LOCATION_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/location]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/location"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
description: 'old value: mqtt.get["rabbitmq","operationalValue/{$DEVICETYPE}/{HOST.HOST}/location","voltra_dev:voltradev","rabbit4voltra"]' description: 'old value: mqtt.get["rabbitmq","operationalValue/{$DEVICETYPE}/{HOST.HOST}/location","voltra_dev:voltradev","rabbit4voltra"]'
@ -248,7 +248,7 @@ zabbix_export:
history: 90d history: 90d
value_type: TEXT value_type: TEXT
master_item: master_item:
key: 'mqtt.trap[operationalValue/location]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/location"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: location value: location
@ -338,26 +338,26 @@ zabbix_export:
items: items:
- uuid: 602290e9f42f4135b548e1cd45abe135 - uuid: 602290e9f42f4135b548e1cd45abe135
name: DENSITY_MQTT name: DENSITY_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/density]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/density"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 87e0a14266984247b81fdc757dea5bde - uuid: 87e0a14266984247b81fdc757dea5bde
name: ERROR_MQTT name: ERROR_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/error]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/error"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 644b0ec2e3d9448da1a69561ec10d19d - uuid: 644b0ec2e3d9448da1a69561ec10d19d
name: SIGNALSTRENGTH_MQTT name: SIGNALSTRENGTH_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/signalstrength]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 67c01d7334a24823832bba74073cf356 - uuid: 67c01d7334a24823832bba74073cf356
name: TEMPERATURE_MQTT name: TEMPERATURE_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/temperature]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/temperature"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
tags: tags:
@ -373,8 +373,8 @@ zabbix_export:
value: operationalValue value: operationalValue
- uuid: 0352c80c749d4d91b386dab9c74ef3c6 - uuid: 0352c80c749d4d91b386dab9c74ef3c6
name: VOLTAGE_MQTT name: VOLTAGE_MQTT
type: TRAP type: ZABBIX_AGENT
key: 'mqtt.trap[operationalValue/voltage]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]'
history: '0' history: '0'
value_type: TEXT value_type: TEXT
- uuid: 7aac8212c94044d28ada982c422f2bf7 - uuid: 7aac8212c94044d28ada982c422f2bf7
@ -387,7 +387,7 @@ zabbix_export:
parameters: parameters:
- $.density - $.density
master_item: master_item:
key: 'mqtt.trap[operationalValue/density]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/density"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: density value: density
@ -463,7 +463,7 @@ zabbix_export:
parameters: parameters:
- $.error - $.error
master_item: master_item:
key: 'mqtt.trap[operationalValue/error]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/error"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: error value: error
@ -484,7 +484,7 @@ zabbix_export:
parameters: parameters:
- $.signalstrength - $.signalstrength
master_item: master_item:
key: 'mqtt.trap[operationalValue/signalstrength]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: signalstrength value: signalstrength
@ -508,7 +508,7 @@ zabbix_export:
parameters: parameters:
- $.temperature - $.temperature
master_item: master_item:
key: 'mqtt.trap[operationalValue/temperature]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/temperature"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: temperature value: temperature
@ -551,7 +551,7 @@ zabbix_export:
parameters: parameters:
- $.timestamp - $.timestamp
master_item: master_item:
key: 'mqtt.trap[operationalValue/signalstrength]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: timestamp value: timestamp
@ -570,7 +570,7 @@ zabbix_export:
parameters: parameters:
- $.timestamp - $.timestamp
master_item: master_item:
key: 'mqtt.trap[operationalValue/voltage]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: timestamp value: timestamp
@ -591,7 +591,7 @@ zabbix_export:
parameters: parameters:
- $.voltage - $.voltage
master_item: master_item:
key: 'mqtt.trap[operationalValue/voltage]' key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]'
tags: tags:
- tag: attributeName - tag: attributeName
value: voltage value: voltage