From bff9ee6d2ed280c315db56014ff0eb27f5166d55 Mon Sep 17 00:00:00 2001 From: Andreas Hilbig Date: Mon, 26 Jan 2026 17:49:31 +0100 Subject: [PATCH] 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. --- README.md | 66 +++++++++ ...e_import_distance_tracker_template.graphql | 125 ++++++++++++++++++ docs/sample_import_templates_mutation.graphql | 7 +- src/api/resolver_helpers.ts | 15 +++ .../templates/zbx_default_templates_vcr.yaml | 70 +++++----- 5 files changed, 245 insertions(+), 38 deletions(-) create mode 100644 docs/sample_import_distance_tracker_template.graphql diff --git a/README.md b/README.md index 0c27fa4..e091486 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Compared to the original Zabbix API, this GraphQL API provides several key enhan * 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. * **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 @@ -71,6 +72,70 @@ npm run prod 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 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**: * [Query Templates](docs/sample_templates_query.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) * **Template Groups**: * [Import Template Groups](docs/sample_import_template_groups_mutation.graphql) diff --git a/docs/sample_import_distance_tracker_template.graphql b/docs/sample_import_distance_tracker_template.graphql new file mode 100644 index 0000000..62dd415 --- /dev/null +++ b/docs/sample_import_distance_tracker_template.graphql @@ -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. diff --git a/docs/sample_import_templates_mutation.graphql b/docs/sample_import_templates_mutation.graphql index 4e41fec..100ddba 100644 --- a/docs/sample_import_templates_mutation.graphql +++ b/docs/sample_import_templates_mutation.graphql @@ -56,14 +56,14 @@ This sample data is based on the `BT_DEVICE_TRACKER` template from `src/testdata } ], "master_item": { - "key": "mqtt.trap[deviceValue/location]" + "key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"deviceValue/location\"]" } }, { "uuid": "380c4a7d752848cba3b5a59a0f9b13c0", "name": "MQTT_LOCATION", - "type": 2, - "key": "mqtt.trap[deviceValue/location]", + "type": 0, + "key": "mqtt.get[\"tcp://mqtt-broker:1883\",\"deviceValue/location\"]", "value_type": 4, "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: #### Item Type (`type`) +- `0`: ZABBIX_AGENT - `2`: ZABBIX_TRAP (TRAP) - `18`: DEPENDANT_ITEM (DEPENDENT) - `21`: SIMULATOR_JAVASCRIPT (JAVASCRIPT) diff --git a/src/api/resolver_helpers.ts b/src/api/resolver_helpers.ts index 1352cdd..8eaca8b 100644 --- a/src/api/resolver_helpers.ts +++ b/src/api/resolver_helpers.ts @@ -16,6 +16,21 @@ function defaultKeyMappingFunction(key: string): string { 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( schema: any, typename: string, sourceFieldMapper: (fieldname: string, parent: any, objectTypeRequested: boolean) => { [p: string]: any } | null): { diff --git a/src/testdata/templates/zbx_default_templates_vcr.yaml b/src/testdata/templates/zbx_default_templates_vcr.yaml index c25fa51..a34abc0 100644 --- a/src/testdata/templates/zbx_default_templates_vcr.yaml +++ b/src/testdata/templates/zbx_default_templates_vcr.yaml @@ -52,11 +52,11 @@ zabbix_export: - filtered error_handler: DISCARD_VALUE master_item: - key: 'mqtt.trap[deviceValue/location]' + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/location"]' - uuid: 380c4a7d752848cba3b5a59a0f9b13c0 name: MQTT_LOCATION - type: TRAP - key: 'mqtt.trap[deviceValue/location]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/location"]' history: '0' value_type: TEXT - uuid: 29faf53c033840c0b1405f8240e30312 @@ -132,27 +132,27 @@ zabbix_export: "timeUntil": v.timeUntil }); master_item: - key: 'mqtt.trap[deviceValue/count]' + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]' - uuid: 905c5f1b6e524bd2b227769a59f4df1b name: MQTT_COUNT - type: TRAP - key: 'mqtt.trap[deviceValue/count]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]' history: '0' value_type: TEXT - uuid: 6fa441872c3140f4adecf39956245603 name: MQTT_DISTANCE - type: TRAP - key: 'mqtt.trap[deviceValue/distance]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/distance"]' value_type: TEXT - uuid: 69d2afa4a0324d818150e9473c3264f3 name: MQTT_NAME - type: TRAP - key: 'mqtt.trap[deviceValue/name]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/name"]' value_type: TEXT - uuid: 45ff9430d27f47a492c98fce03fc7962 name: MQTT_SERVICE_DATA - type: TRAP - key: 'mqtt.trap[deviceValue/ServiceData]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/ServiceData"]' value_type: TEXT - uuid: 3bf0d3017ea54e1da2a764c3f96bf97e name: count @@ -164,7 +164,7 @@ zabbix_export: parameters: - $.count master_item: - key: 'mqtt.trap[deviceValue/count]' + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]' - uuid: f0d1fc72e2154613b349be86c6bdcfd6 name: timeFrom type: DEPENDENT @@ -179,7 +179,7 @@ zabbix_export: - 'T(\d\d:\d\d:\d\d):' - \1 master_item: - key: 'mqtt.trap[deviceValue/count]' + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]' - uuid: e55bf604808f4eb4a964ebeefdd9eb9e name: timeUntil type: DEPENDENT @@ -194,7 +194,7 @@ zabbix_export: - 'T(\d\d:\d\d:\d\d):' - \1 master_item: - key: 'mqtt.trap[deviceValue/count]' + key: 'mqtt.get["tcp://mqtt-broker:1883","deviceValue/count"]' tags: - tag: class value: roadwork @@ -236,8 +236,8 @@ zabbix_export: items: - uuid: 4ad4d9a769744615816d190c34cb49c7 name: GPS_LOCATION_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/location]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/location"]' history: '0' value_type: TEXT description: 'old value: mqtt.get["rabbitmq","operationalValue/{$DEVICETYPE}/{HOST.HOST}/location","voltra_dev:voltradev","rabbit4voltra"]' @@ -248,7 +248,7 @@ zabbix_export: history: 90d value_type: TEXT master_item: - key: 'mqtt.trap[operationalValue/location]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/location"]' tags: - tag: attributeName value: location @@ -338,26 +338,26 @@ zabbix_export: items: - uuid: 602290e9f42f4135b548e1cd45abe135 name: DENSITY_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/density]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/density"]' history: '0' value_type: TEXT - uuid: 87e0a14266984247b81fdc757dea5bde name: ERROR_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/error]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/error"]' history: '0' value_type: TEXT - uuid: 644b0ec2e3d9448da1a69561ec10d19d name: SIGNALSTRENGTH_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/signalstrength]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]' history: '0' value_type: TEXT - uuid: 67c01d7334a24823832bba74073cf356 name: TEMPERATURE_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/temperature]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/temperature"]' history: '0' value_type: TEXT tags: @@ -373,8 +373,8 @@ zabbix_export: value: operationalValue - uuid: 0352c80c749d4d91b386dab9c74ef3c6 name: VOLTAGE_MQTT - type: TRAP - key: 'mqtt.trap[operationalValue/voltage]' + type: ZABBIX_AGENT + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]' history: '0' value_type: TEXT - uuid: 7aac8212c94044d28ada982c422f2bf7 @@ -387,7 +387,7 @@ zabbix_export: parameters: - $.density master_item: - key: 'mqtt.trap[operationalValue/density]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/density"]' tags: - tag: attributeName value: density @@ -463,7 +463,7 @@ zabbix_export: parameters: - $.error master_item: - key: 'mqtt.trap[operationalValue/error]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/error"]' tags: - tag: attributeName value: error @@ -484,7 +484,7 @@ zabbix_export: parameters: - $.signalstrength master_item: - key: 'mqtt.trap[operationalValue/signalstrength]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]' tags: - tag: attributeName value: signalstrength @@ -508,7 +508,7 @@ zabbix_export: parameters: - $.temperature master_item: - key: 'mqtt.trap[operationalValue/temperature]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/temperature"]' tags: - tag: attributeName value: temperature @@ -551,7 +551,7 @@ zabbix_export: parameters: - $.timestamp master_item: - key: 'mqtt.trap[operationalValue/signalstrength]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/signalstrength"]' tags: - tag: attributeName value: timestamp @@ -570,7 +570,7 @@ zabbix_export: parameters: - $.timestamp master_item: - key: 'mqtt.trap[operationalValue/voltage]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]' tags: - tag: attributeName value: timestamp @@ -591,7 +591,7 @@ zabbix_export: parameters: - $.voltage master_item: - key: 'mqtt.trap[operationalValue/voltage]' + key: 'mqtt.get["tcp://mqtt-broker:1883","operationalValue/voltage"]' tags: - tag: attributeName value: voltage