feat: optimize Zabbix queries and enhance specialized device support
- Implement query optimization (reduced output, parameter skipping) to minimize Zabbix API traffic. - Add indirect dependency handling: deviceType implies tags and state implies items. - Move schema extensions to samples/extensions/ to clarify their role as samples. - Enhance DistanceTrackerDevice with String time fields to support optional date portions. - Ensure allDevices strictly filters by deviceType and populates the field in results. - Refactor runAllRegressionTests mutation to use internal unique names and improve stability. - Fix unnecessary Zabbix API calls for item preprocessing during template and host imports. - Update documentation including cookbook recipes, test specifications, and optimization guides. - Add extensive unit, integration, and regression tests covering all implemented changes. - Update docker-compose.yml to mount the samples/ directory as a volume. - Update IntelliJ .idea run configurations to reflect the new sample extension paths.
This commit is contained in:
parent
97a0f70fd6
commit
b646b8c606
28 changed files with 551 additions and 74 deletions
2
.idea/runConfigurations/index_ts.xml
generated
2
.idea/runConfigurations/index_ts.xml
generated
|
|
@ -2,7 +2,7 @@
|
|||
<configuration default="false" name="index.ts" type="NodeJSConfigurationType" path-to-node="wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node" nameIsGenerated="true" path-to-js-file="src/index.ts" node-parameters="--import tsx" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="ADDITIONAL_RESOLVERS" value="SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice" />
|
||||
<env name="ADDITIONAL_SCHEMAS" value="./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql,./schema/extensions/location_tracker_commons.graphql" />
|
||||
<env name="ADDITIONAL_SCHEMAS" value="./samples/extensions/display_devices.graphql,./samples/extensions/location_tracker_devices.graphql,./samples/extensions/location_tracker_commons.graphql" />
|
||||
<env name="DEBUG" value="device-control-center-api:*" />
|
||||
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
||||
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
|
||||
|
|
|
|||
50
.idea/workspace.xml
generated
50
.idea/workspace.xml
generated
|
|
@ -5,18 +5,34 @@
|
|||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24 This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP). Changes: - Environment & CI/CD: - Set Node.js version to >=24 in package.json and .nvmrc. - Updated Dockerfile to use Node 24 base image. - Updated @types/node to ^24.10.9. - Documentation: - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping. - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation. - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol. - Added readme.improvement.plan.md to track documentation evolution. - Enhanced all how-to guides with improved cross-references and up-to-date information. - Guidelines: - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0). - Infrastructure & Code: - Updated docker-compose.yml with Apollo MCP server integration. - Refined configuration and schema handling in src/api/ and src/datasources/. - Synchronized generated TypeScript types with schema updates.">
|
||||
<change afterPath="$PROJECT_DIR$/docs/queries/sample_distance_tracker_test_query.graphql" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/test/schema_dependent_queries.test.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/runConfigurations/index_ts.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/runConfigurations/index_ts.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docker-compose.yml" beforeDir="false" afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/howtos/cookbook.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/cookbook.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/queries/sample_import_weather_sensor_template.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/docs/queries/sample_import_weather_sensor_template.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/howtos/query_optimization.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/query_optimization.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/howtos/schema.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/schema.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/queries/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/queries/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/tests.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/tests.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/mcp/operations/runAllRegressionTests.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/mcp/operations/runAllRegressionTests.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/extensions/display_devices.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/display_devices.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/extensions/ground_value_checker.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/ground_value_checker.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/extensions/location_tracker_commons.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/location_tracker_commons.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/extensions/location_tracker_devices.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/location_tracker_devices.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/extensions/weather_sensor.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/weather_sensor.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/schema/mutations.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/schema/mutations.graphql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/api/resolvers.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/resolvers.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/datasources/graphql-params-to-zabbix-output.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/graphql-params-to-zabbix-output.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-hosts.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hosts.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-templates.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-templates.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/execution/host_importer.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/host_importer.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/execution/template_importer.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/template_importer.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/model/model_enum_values.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/model/model_enum_values.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/schema/generated/graphql.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/schema/generated/graphql.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/test/host_importer.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/host_importer.test.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/test/template_importer.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_importer.test.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/test/host_query.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/host_query.test.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/test/query_optimization.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/query_optimization.test.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -27,13 +43,13 @@
|
|||
<execution />
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="149" />
|
||||
<option name="cachedIndexableFilesCount" value="163" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="license" />
|
||||
<entry key="$PROJECT_DIR$" value="vlsv" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
|
|
@ -95,7 +111,7 @@
|
|||
"go.import.settings.migrated": "true",
|
||||
"javascript.preferred.runtime.type.id": "node",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/src",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
|
|
@ -107,8 +123,8 @@
|
|||
"npm.copy-schema.executor": "Run",
|
||||
"npm.prod.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"settings.editor.selected.configurable": "ml.llm.mcp",
|
||||
"settings.editor.splitter.proportion": "0.28812414",
|
||||
"settings.editor.selected.configurable": "junie.mcp",
|
||||
"settings.editor.splitter.proportion": "0.23751687",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
|
|
@ -116,16 +132,16 @@
|
|||
}]]></component>
|
||||
<component name="RecapSpentCounter">
|
||||
<option name="endsOfQuotaMs" value="1772398800000" />
|
||||
<option name="spentUsd" value="0.01011225" />
|
||||
<option name="spentUsd" value="0.0915201" />
|
||||
</component>
|
||||
<component name="RecapUselessUpdatesCounter">
|
||||
<option name="suspendCountdown" value="8" />
|
||||
<option name="suspendCountdown" value="0" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\docs" />
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\src" />
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\dist" />
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\docs" />
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\src\testdata\templates" />
|
||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\src\test" />
|
||||
</key>
|
||||
|
|
@ -153,7 +169,7 @@
|
|||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
<env name="ADDITIONAL_RESOLVERS" value="SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice" />
|
||||
<env name="ADDITIONAL_SCHEMAS" value="./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql,./schema/extensions/location_tracker_commons.graphql" />
|
||||
<env name="ADDITIONAL_SCHEMAS" value="./samples/extensions/display_devices.graphql,./samples/extensions/location_tracker_devices.graphql,./samples/extensions/location_tracker_commons.graphql" />
|
||||
<env name="DEBUG" value="device-control-center-api:*" />
|
||||
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
||||
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
|
||||
|
|
@ -185,11 +201,11 @@
|
|||
</list>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="npm.prod" />
|
||||
<item itemvalue="npm.copy-schema" />
|
||||
<item itemvalue="npm.test" />
|
||||
<item itemvalue="npm.test" />
|
||||
<item itemvalue="npm.prod" />
|
||||
<item itemvalue="npm.copy-schema" />
|
||||
<item itemvalue="npm.prod" />
|
||||
<item itemvalue="npm.copy-schema" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
|
|
@ -488,7 +504,7 @@
|
|||
</line-breakpoint>
|
||||
<line-breakpoint enabled="true" type="javascript">
|
||||
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
|
||||
<line>213</line>
|
||||
<line>253</line>
|
||||
<option name="timeStamp" value="6" />
|
||||
</line-breakpoint>
|
||||
</breakpoints>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ The Zabbix GraphQL API acts as a wrapper and enhancer for the native Zabbix JSON
|
|||
- *Reference*: `schema/mutations.graphql` (importHosts, importTemplates, importUserRights, etc.), `docs/sample_import_*.graphql`
|
||||
|
||||
- **Dynamic Schema Extension**: Extend the schema without code changes using environment variables
|
||||
- *Reference*: `src/api/schema.ts`, `schema/extensions/`, `src/common_utils.ts` (ADDITIONAL_SCHEMAS, ADDITIONAL_RESOLVERS)
|
||||
- *Reference*: `src/api/schema.ts`, `samples/extensions/` (sample extensions), `src/common_utils.ts` (ADDITIONAL_SCHEMAS, ADDITIONAL_RESOLVERS)
|
||||
|
||||
- **Permission System**: Role-based access control using Zabbix template groups
|
||||
- *Reference*: `schema/api_commons.graphql` (Permission enum, PermissionRequest), `src/api/resolvers.ts` (hasPermissions, userPermissions), `docs/sample_import_permissions_template_groups_mutation.graphql`
|
||||
|
|
@ -213,7 +213,7 @@ HOST_GROUP_FILTER_DEFAULT=Roadwork/Devices/*
|
|||
HOST_TYPE_FILTER_DEFAULT=Roadwork/Devices
|
||||
|
||||
# Schema Extensions (No-Code)
|
||||
ADDITIONAL_SCHEMAS=./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql,./schema/extensions/location_tracker_commons.graphql
|
||||
ADDITIONAL_SCHEMAS=./samples/extensions/display_devices.graphql,./samples/extensions/location_tracker_devices.graphql,./samples/extensions/location_tracker_commons.graphql
|
||||
ADDITIONAL_RESOLVERS=SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice
|
||||
|
||||
# Logging
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ services:
|
|||
environment:
|
||||
- SCHEMA_PATH=/usr/app/dist/schema/
|
||||
- ZABBIX_DEVELOPMENT_TOKEN=${ZABBIX_DEVELOPMENT_TOKEN}
|
||||
volumes:
|
||||
- ./samples:/usr/app/dist/samples
|
||||
|
||||
apollo-mcp-server:
|
||||
image: ghcr.io/apollographql/apollo-mcp-server:latest
|
||||
|
|
|
|||
|
|
@ -73,12 +73,14 @@ Compare the GraphQL response with the expected output described in the Zabbix do
|
|||
|
||||
This recipe shows how to add support for a new specialized device type without modifying the core API code. We will use the `DistanceTrackerDevice` as an example.
|
||||
|
||||
> **Important**: Schema extensions are not part of the core API source code. They are loaded dynamically at runtime via environment variables. The extensions provided in the `samples/extensions/` directory of this repository are **samples** to demonstrate how to use this mechanism. You can place your own extension files in any directory accessible by the API server.
|
||||
|
||||
### 📋 Prerequisites
|
||||
- Zabbix Template Group `Templates/Roadwork/Devices` exists.
|
||||
- Zabbix GraphQL API is running.
|
||||
|
||||
### 🛠️ Step 1: Define the Schema Extension
|
||||
Create a new `.graphql` file in `schema/extensions/` (e.g. `distance_tracker.graphql`).
|
||||
Create a new `.graphql` file in `samples/extensions/` (e.g. `distance_tracker.graphql`).
|
||||
|
||||
> **Advice**: A new device type must always implement both the `Host` and `Device` interfaces to ensure compatibility with the API's core logic and resolvers.
|
||||
|
||||
|
|
@ -102,20 +104,20 @@ type DistanceTrackerState implements DeviceState {
|
|||
}
|
||||
|
||||
type DistanceTrackerValues {
|
||||
timeFrom: Time
|
||||
timeUntil: Time
|
||||
timeFrom: String
|
||||
timeUntil: String
|
||||
count: Int
|
||||
# The distances are modelled using a type which is already defined in location_tracker_commons.graphql
|
||||
distances: [SensorDistanceValue!]
|
||||
}
|
||||
```
|
||||
|
||||
> **Reference**: This example is based on the already prepared sample: [location_tracker_devices.graphql](../../schema/extensions/location_tracker_devices.graphql).
|
||||
> **Reference**: This example is based on the already prepared sample: [location_tracker_devices.graphql](../../samples/extensions/location_tracker_devices.graphql).
|
||||
|
||||
### ⚙️ Step 2: Configure Environment Variables
|
||||
Add the new schema and resolver to your `.env` file:
|
||||
```env
|
||||
ADDITIONAL_SCHEMAS=./schema/extensions/distance_tracker.graphql,./schema/extensions/location_tracker_commons.graphql
|
||||
ADDITIONAL_SCHEMAS=./samples/extensions/distance_tracker.graphql,./samples/extensions/location_tracker_commons.graphql
|
||||
ADDITIONAL_RESOLVERS=DistanceTrackerDevice
|
||||
```
|
||||
Restart the API server.
|
||||
|
|
@ -203,7 +205,7 @@ This recipe demonstrates how to extend the schema with new device types that ret
|
|||
- The device has geo-coordinates set via user macros (e.g. `{$LAT}` and `{$LON}`).
|
||||
|
||||
### 🛠️ Step 1: Define the Schema Extension
|
||||
Create a new `.graphql` file in `schema/extensions/` (e.g. `weather_sensor.graphql` or `ground_value_checker.graphql`).
|
||||
Create a new `.graphql` file in `samples/extensions/` (e.g. `weather_sensor.graphql` or `ground_value_checker.graphql`).
|
||||
|
||||
**Sample: Weather Sensor**
|
||||
```graphql
|
||||
|
|
@ -262,7 +264,7 @@ type GroundValues {
|
|||
### ⚙️ Step 2: Register the Resolver
|
||||
Add the new types and schemas to your `.env` file to enable the dynamic resolver:
|
||||
```env
|
||||
ADDITIONAL_SCHEMAS=./schema/extensions/weather_sensor.graphql,./schema/extensions/ground_value_checker.graphql
|
||||
ADDITIONAL_SCHEMAS=./samples/extensions/weather_sensor.graphql,./samples/extensions/ground_value_checker.graphql
|
||||
ADDITIONAL_RESOLVERS=WeatherSensorDevice,GroundValueChecker
|
||||
```
|
||||
Restart the API server to apply the changes.
|
||||
|
|
@ -344,6 +346,30 @@ Create a host, assign it macros for coordinates, and query its state.
|
|||
|
||||
---
|
||||
|
||||
## 🍳 Recipe: Testing Specialized Device Types
|
||||
|
||||
This recipe shows how to execute a comprehensive query to verify the state and configuration of specialized device types, such as the `DistanceTrackerDevice`. This is useful for validating that your schema extensions and hierarchical mappings are working correctly.
|
||||
|
||||
### 📋 Prerequisites
|
||||
- Zabbix GraphQL API is running.
|
||||
- The schema has been extended with the `DistanceTrackerDevice` type (see [Recipe: Extending Schema with a New Device Type](#-recipe-extending-schema-with-a-new-device-type)). Sample extensions can be found in the `samples/extensions` directory.
|
||||
- At least one host with `deviceType` set to `DistanceTrackerDevice` exists in Zabbix.
|
||||
|
||||
### 🛠️ Step 1: Get the Sample Query
|
||||
1. **Open the Sample**: Open [docs/queries/sample_distance_tracker_test_query.graphql](../queries/sample_distance_tracker_test_query.graphql).
|
||||
2. **Copy the Query**: Copy the GraphQL code block under the `### Query` header.
|
||||
|
||||
### 🚀 Step 2: Execution/Action
|
||||
Execute the query against your GraphQL endpoint. This query retrieves information from `allHostGroups`, `allDevices`, and `allHosts`, using inline fragments to access fields specific to `DistanceTrackerDevice`.
|
||||
|
||||
### ✅ Step 3: Verification
|
||||
Check the response for the following:
|
||||
- **apiVersion** and **zabbixVersion** are returned.
|
||||
- **allHostGroups** contains the expected groups.
|
||||
- **allDevices** and **allHosts** include your `DistanceTrackerDevice` with its specialized `state` (count, timeFrom, timeUntil) and `tags` (deviceWidgetPreview).
|
||||
|
||||
---
|
||||
|
||||
## 🍳 Recipe: Provisioning a New Host
|
||||
|
||||
### 📋 Prerequisites
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ The `GraphqlParamsToNeededZabbixOutput` class provides static methods to map Gra
|
|||
### 3. Resolver Integration
|
||||
Resolvers use the mapper to determine the required output and pass it to the datasource:
|
||||
```typescript
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(info);
|
||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
||||
.executeRequestThrowError(dataSources.zabbixAPI, new ParsedArgs(args), output);
|
||||
```
|
||||
|
|
@ -30,6 +30,7 @@ return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
|||
### 4. Indirect Dependencies
|
||||
Some GraphQL fields are not directly returned by Zabbix but are computed from other data. The optimization logic ensures these dependencies are handled:
|
||||
- **`state`**: Requesting the `state` field on a `Device` requires Zabbix `items`. The mapper automatically adds `items` to the requested output if `state` is present.
|
||||
- **`deviceType`**: Requesting `deviceType` requires Zabbix `tags` (or `inheritedTags`). This is needed because the `deviceType` is resolved from a Zabbix tag and will be empty otherwise. The optimization logic ensures that `selectTags` and `selectInheritedTags` are not skipped when `deviceType` is requested.
|
||||
|
||||
## 🛠️ Configuration
|
||||
Optimization rules are defined in the constructor of specialized `ZabbixRequest` classes.
|
||||
|
|
@ -37,7 +38,7 @@ Optimization rules are defined in the constructor of specialized `ZabbixRequest`
|
|||
### 📋 Supported Optimizations
|
||||
- **Hosts & Devices**:
|
||||
- `selectParentTemplates` skipped if `parentTemplates` not requested.
|
||||
- `selectTags` and `selectInheritedTags` skipped if `tags` not requested.
|
||||
- `selectTags` and `selectInheritedTags` skipped if `tags` (or `deviceType`) not requested.
|
||||
- `selectHostGroups` skipped if `hostgroups` not requested.
|
||||
- `selectItems` skipped if `items` (or `state`) not requested.
|
||||
- `selectInventory` skipped if `inventory` not requested.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The GraphQL schema is located in the `../../schema/` directory and consists of:
|
|||
- `zabbix.graphql` - Zabbix-specific types (see detailed documentation in file comments)
|
||||
- `device_value_commons.graphql` - Common value types (see detailed documentation in file comments)
|
||||
- `api_commons.graphql` - Common API types and permission system (see detailed documentation in file comments)
|
||||
- `extensions/` - Custom device type extensions
|
||||
- `samples/extensions/` - Sample device type extensions (not part of the core source)
|
||||
|
||||
For comprehensive understanding of each operation, read the detailed comments in the respective schema files.
|
||||
|
||||
|
|
@ -38,7 +38,8 @@ The `Location` type represents geographical information from Zabbix host invento
|
|||
Extend the schema without code changes using environment variables:
|
||||
|
||||
```bash
|
||||
ADDITIONAL_SCHEMAS=./schema/extensions/display_devices.graphql,./schema/extensions/location_tracker_devices.graphql
|
||||
# Extensions can be located anywhere; samples are provided in samples/extensions/
|
||||
ADDITIONAL_SCHEMAS=./samples/extensions/display_devices.graphql,./samples/extensions/location_tracker_devices.graphql
|
||||
ADDITIONAL_RESOLVERS=SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ This directory contains practical examples of GraphQL operations for the Zabbix
|
|||
- [Query All Hosts](./sample_all_hosts_query.graphql): Retrieve basic host information and inventory.
|
||||
- [Import Hosts](./sample_import_hosts_mutation.graphql): Create or update multiple hosts with tags and group assignments.
|
||||
- [Query All Devices](./sample_all_devices_query.graphql): Query specialized devices using the `allDevices` query.
|
||||
- [Distance Tracker Test Query](./sample_distance_tracker_test_query.graphql): Comprehensive query for testing specialized `DistanceTrackerDevice` types.
|
||||
|
||||
### 📄 Templates
|
||||
- [Query Templates](./sample_templates_query.graphql): List available templates and their items.
|
||||
|
|
|
|||
118
docs/queries/sample_distance_tracker_test_query.graphql
Normal file
118
docs/queries/sample_distance_tracker_test_query.graphql
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
### Query
|
||||
This query demonstrates how to retrieve data from multiple sources, including specialized device types like `DistanceTrackerDevice`.
|
||||
|
||||
> **Precondition**: This query will only work if the GraphQL schema has been extended with the `DistanceTrackerDevice` type (see the sample in `samples/extensions/location_tracker_devices.graphql`).
|
||||
|
||||
```graphql
|
||||
query DistanceTrackerDeviceTest {
|
||||
apiVersion
|
||||
zabbixVersion
|
||||
allHostGroups(search_name: "Roadwork/Devices/*") {
|
||||
groupid
|
||||
name
|
||||
}
|
||||
allDevices {
|
||||
deviceType
|
||||
host
|
||||
name
|
||||
... on DistanceTrackerDevice {
|
||||
state {
|
||||
current {
|
||||
count
|
||||
timeFrom
|
||||
timeUntil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allHosts {
|
||||
hostid
|
||||
host
|
||||
name
|
||||
deviceType
|
||||
... on Device {
|
||||
tags {
|
||||
deviceWidgetPreview {
|
||||
TOP_LEFT {
|
||||
key
|
||||
emptyValue
|
||||
unit
|
||||
value_font_size
|
||||
g_value_transform
|
||||
unit_font_size
|
||||
g_unit_transform
|
||||
}
|
||||
TOP_RIGHT {
|
||||
key
|
||||
emptyValue
|
||||
unit
|
||||
value_font_size
|
||||
g_value_transform
|
||||
unit_font_size
|
||||
g_unit_transform
|
||||
}
|
||||
BOTTOM_LEFT {
|
||||
key
|
||||
emptyValue
|
||||
unit
|
||||
value_font_size
|
||||
g_value_transform
|
||||
unit_font_size
|
||||
g_unit_transform
|
||||
}
|
||||
BOTTOM_RIGHT {
|
||||
key
|
||||
emptyValue
|
||||
unit
|
||||
value_font_size
|
||||
g_value_transform
|
||||
unit_font_size
|
||||
g_unit_transform
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DistanceTrackerDevice {
|
||||
state {
|
||||
current {
|
||||
count
|
||||
timeFrom
|
||||
timeUntil
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ZabbixHost {
|
||||
items {
|
||||
itemid
|
||||
name
|
||||
key_
|
||||
hostid
|
||||
lastclock
|
||||
lastvalue
|
||||
value_type
|
||||
attributeName
|
||||
status
|
||||
type
|
||||
}
|
||||
}
|
||||
... on GenericDevice {
|
||||
deviceType
|
||||
state {
|
||||
generic: current
|
||||
}
|
||||
}
|
||||
... on SinglePanelDevice {
|
||||
deviceType
|
||||
state {
|
||||
current {
|
||||
values {
|
||||
contentIndex
|
||||
contentKey
|
||||
contentText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -71,6 +71,9 @@ This document outlines the test cases and coverage for the Zabbix GraphQL API.
|
|||
- **TC-DOCS-01**: Validate all Zabbix documentation sample queries.
|
||||
- **TC-MCP-01**: Validate all MCP operation files against the schema.
|
||||
|
||||
### Schema-dependent Tests
|
||||
- **TC-SCHEMA-01**: Verify comprehensive query for `DistanceTrackerDevice` works correctly when schema is extended.
|
||||
|
||||
### End-to-End (E2E) Tests
|
||||
- **TC-E2E-01**: Run a complete smoketest using MCP (creates template, group, and host, verifies, and cleans up).
|
||||
- **TC-E2E-02**: Run all regression tests to verify critical system behavior and prevent known issues.
|
||||
|
|
@ -87,6 +90,7 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
|||
- **Dependent Items**: Verifies that templates with master and dependent items can be imported successfully, correctly resolving the dependency within the same import operation.
|
||||
- **State sub-properties**: Verifies that requesting device state sub-properties correctly triggers the retrieval of required Zabbix items, even if `items` is not explicitly requested (verifying the indirect dependency logic).
|
||||
- **Negative Optimization (allDevices)**: Verifies that items are NOT requested from Zabbix if neither `items` nor `state` (or state sub-properties) are requested within the `allDevices` query.
|
||||
- **allDevices deviceType filter**: Verifies that the `allDevices` query only returns hosts that have a `deviceType` tag, and that the `deviceType` field is populated for all results.
|
||||
|
||||
## ✅ Test Coverage Checklist
|
||||
|
||||
|
|
@ -138,6 +142,7 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
|||
| TC-CONF-07 | Parse Zabbix Args | Unit | Jest | [src/test/zabbix_api_args_parser.test.ts](../src/test/zabbix_api_args_parser.test.ts) |
|
||||
| TC-DOCS-01 | Zabbix Docs Samples Integration | Integration | Jest | [src/test/zabbix_docs_samples.test.ts](../src/test/zabbix_docs_samples.test.ts) |
|
||||
| TC-MCP-01 | MCP Operations Validation | Integration | Jest | [src/test/mcp_operations_validation.test.ts](../src/test/mcp_operations_validation.test.ts) |
|
||||
| TC-SCHEMA-01 | DistanceTrackerDevice Query | Integration | Jest | [src/test/schema_dependent_queries.test.ts](../src/test/schema_dependent_queries.test.ts) |
|
||||
| TC-E2E-01 | Run complete smoketest | E2E | GraphQL / MCP | [mcp/operations/runSmoketest.graphql](../mcp/operations/runSmoketest.graphql) |
|
||||
| TC-E2E-02 | Run all regression tests | E2E | GraphQL / MCP | [mcp/operations/runAllRegressionTests.graphql](../mcp/operations/runAllRegressionTests.graphql) |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Runs all regression tests.
|
||||
# Variables: hostName, groupName
|
||||
mutation RunAllRegressionTests($hostName: String!, $groupName: String!) {
|
||||
runAllRegressionTests(hostName: $hostName, groupName: $groupName) {
|
||||
mutation RunAllRegressionTests {
|
||||
runAllRegressionTests {
|
||||
success
|
||||
message
|
||||
steps {
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ type DistanceTrackerValues {
|
|||
"""
|
||||
Start of time interval for the delivered device counting value.
|
||||
"""
|
||||
timeFrom: Time
|
||||
timeFrom: String
|
||||
"""
|
||||
End of time interval for the delivered device counting value.
|
||||
"""
|
||||
timeUntil: Time
|
||||
timeUntil: String
|
||||
|
||||
"""
|
||||
Number of unique device keys detected between timeFrom and timeUntil.
|
||||
|
|
@ -142,12 +142,7 @@ type Mutation {
|
|||
"""
|
||||
Runs all regression tests.
|
||||
"""
|
||||
runAllRegressionTests(
|
||||
"""Technical name for the test host."""
|
||||
hostName: String!,
|
||||
"""Technical name for the test host group."""
|
||||
groupName: String!
|
||||
): SmoketestResponse!
|
||||
runAllRegressionTests: SmoketestResponse!
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export function createResolvers(): Resolvers {
|
|||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(info);
|
||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
|
||||
|
|
@ -120,7 +120,7 @@ export function createResolvers(): Resolvers {
|
|||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllDevices(args, info);
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllDevices(info);
|
||||
return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
|
||||
|
|
@ -133,7 +133,7 @@ export function createResolvers(): Resolvers {
|
|||
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
||||
args.search_name = Config.HOST_GROUP_FILTER_DEFAULT
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHostGroups(args, info);
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHostGroups(info);
|
||||
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
|
||||
)
|
||||
|
|
@ -173,7 +173,7 @@ export function createResolvers(): Resolvers {
|
|||
name: args.name_pattern
|
||||
}
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
|
||||
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
||||
},
|
||||
|
|
@ -287,11 +287,11 @@ export function createResolvers(): Resolvers {
|
|||
}: any) => {
|
||||
return SmoketestExecutor.runSmoketest(args.hostName, args.templateName, args.groupName, zabbixAuthToken, cookie)
|
||||
},
|
||||
runAllRegressionTests: async (_parent: any, args: any, {
|
||||
runAllRegressionTests: async (_parent: any, _args: any, {
|
||||
zabbixAuthToken,
|
||||
cookie
|
||||
}: any) => {
|
||||
return RegressionTestExecutor.runAllRegressionTests(args.hostName, args.groupName, zabbixAuthToken, cookie)
|
||||
return RegressionTestExecutor.runAllRegressionTests(zabbixAuthToken, cookie)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
import {GraphQLResolveInfo} from "graphql";
|
||||
import {getRequestedFields} from "../api/graphql_utils.js";
|
||||
import {
|
||||
QueryAllDevicesArgs,
|
||||
QueryAllHostGroupsArgs,
|
||||
QueryAllHostsArgs,
|
||||
QueryTemplatesArgs
|
||||
} from "../schema/generated/graphql.js";
|
||||
|
||||
export class GraphqlParamsToNeededZabbixOutput {
|
||||
static mapAllHosts(args: QueryAllHostsArgs, info: GraphQLResolveInfo): string[] {
|
||||
static mapAllHosts(info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapAllDevices(args: QueryAllDevicesArgs, info: GraphQLResolveInfo): string[] {
|
||||
static mapAllDevices(info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapAllHostGroups(args: QueryAllHostGroupsArgs, info: GraphQLResolveInfo): string[] {
|
||||
static mapAllHostGroups(info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapTemplates(args: QueryTemplatesArgs, info: GraphQLResolveInfo): string[] {
|
||||
static mapTemplates(info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
|||
this.skippableZabbixParams.set("selectTags", "tags");
|
||||
this.skippableZabbixParams.set("selectInheritedTags", "tags");
|
||||
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
|
||||
this.impliedFields.set("deviceType", ["tags"]);
|
||||
this.impliedFields.set("hostType", ["tags"]);
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export class TemplateHelper {
|
|||
// Use name_pattern which now searches both visibility name and technical name (host)
|
||||
let templates = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ParsedArgs({
|
||||
name_pattern: templateName
|
||||
}))
|
||||
}), ["templateid", "host"])
|
||||
|
||||
if (isZabbixErrorResult(templates) || !templates?.length) {
|
||||
logger.error(`Unable to find templateName=${templateName}`)
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export class HostImporter {
|
|||
{
|
||||
tag_deviceType: deviceType
|
||||
}
|
||||
));
|
||||
), ["templateid"]);
|
||||
|
||||
if (templates?.length) {
|
||||
result = Number(templates[0].templateid)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,16 @@ import {
|
|||
ZabbixQueryHostsGenericRequestWithItems
|
||||
} from "../datasources/zabbix-hosts.js";
|
||||
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
||||
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
||||
import {isZabbixErrorResult, ParsedArgs, ZabbixRequest} from "../datasources/zabbix-request.js";
|
||||
|
||||
export class RegressionTestExecutor {
|
||||
public static async runAllRegressionTests(hostName: string, groupName: string, zabbixAuthToken?: string, cookie?: string): Promise<SmoketestResponse> {
|
||||
public static async runAllRegressionTests(zabbixAuthToken?: string, cookie?: string): Promise<SmoketestResponse> {
|
||||
const steps: SmoketestStep[] = [];
|
||||
let success = true;
|
||||
|
||||
const hostName = "REG_HOST_" + Math.random().toString(36).substring(7);
|
||||
const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7);
|
||||
|
||||
try {
|
||||
// Regression 1: Locations query argument order
|
||||
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
|
||||
|
|
@ -309,8 +312,15 @@ export class RegressionTestExecutor {
|
|||
|
||||
optSuccess = optSuccess && hasSelectItems3 && hasOutput3;
|
||||
|
||||
// 4. Test indirect dependencies: deviceType implies tags
|
||||
const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]);
|
||||
const hasSelectTags4 = "selectTags" in testParams4;
|
||||
const hasOutput4 = Array.isArray(testParams4.output) && testParams4.output.includes("hostid");
|
||||
|
||||
optSuccess = optSuccess && hasSelectTags4 && hasOutput4;
|
||||
|
||||
if (!optSuccess) {
|
||||
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}`);
|
||||
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}, hasSelectTags4: ${hasSelectTags4}, hasOutput4: ${hasOutput4}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||
|
|
@ -469,6 +479,57 @@ export class RegressionTestExecutor {
|
|||
});
|
||||
if (!optNegSuccess) success = false;
|
||||
|
||||
// Regression 12: allDevices deviceType filter
|
||||
// Verifies that allDevices only returns hosts with a deviceType tag
|
||||
const devHostNameWithTag = "REG_DEV_WITH_TAG_" + Math.random().toString(36).substring(7);
|
||||
const devHostNameWithoutTag = "REG_DEV_WITHOUT_TAG_" + Math.random().toString(36).substring(7);
|
||||
|
||||
// Get groupid for hostGroupName
|
||||
const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie)
|
||||
.executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName }));
|
||||
const regGroupId = Array.isArray(groupQuery) && groupQuery[0]?.groupid;
|
||||
|
||||
if (regGroupId) {
|
||||
await HostImporter.importHosts([{
|
||||
deviceKey: devHostNameWithTag,
|
||||
deviceType: "RegressionDevice",
|
||||
groupNames: [hostGroupName]
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
await new ZabbixRequest("host.create", zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||
host: devHostNameWithoutTag,
|
||||
name: devHostNameWithoutTag,
|
||||
groups: [{ groupid: regGroupId }]
|
||||
}));
|
||||
|
||||
const allDevicesResult: any = await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||
.executeRequestReturnError(zabbixAPI, new ZabbixQueryDevicesArgs({
|
||||
filter_host: [devHostNameWithTag, devHostNameWithoutTag]
|
||||
}), ["name", "host", "hostid", "deviceType"]);
|
||||
|
||||
if (isZabbixErrorResult(allDevicesResult)) {
|
||||
steps.push({
|
||||
name: "REG-DEV-FILTER: allDevices deviceType filter",
|
||||
success: false,
|
||||
message: `Zabbix error: ${allDevicesResult.error.message}`
|
||||
});
|
||||
} else {
|
||||
const hasHostWithTag = allDevicesResult.some((d: any) => d.host === devHostNameWithTag);
|
||||
const hasHostWithoutTag = allDevicesResult.some((d: any) => d.host === devHostNameWithoutTag);
|
||||
const devTypeNotNull = allDevicesResult.length > 0 && allDevicesResult.every((d: any) => d.deviceType !== null && d.deviceType !== undefined && d.deviceType !== "");
|
||||
|
||||
const devFilterSuccess = hasHostWithTag && !hasHostWithoutTag && devTypeNotNull;
|
||||
steps.push({
|
||||
name: "REG-DEV-FILTER: allDevices deviceType filter",
|
||||
success: devFilterSuccess,
|
||||
message: devFilterSuccess
|
||||
? `allDevices correctly filtered out hosts without deviceType tag`
|
||||
: `Failed: withTag=${hasHostWithTag}, withoutTag=${hasHostWithoutTag}, typeNotNull=${devTypeNotNull}, result=${JSON.stringify(allDevicesResult)}`
|
||||
});
|
||||
if (!devFilterSuccess) success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
||||
const groupResult = await HostImporter.importHostGroups([{
|
||||
groupName: groupName
|
||||
|
|
@ -486,6 +547,8 @@ export class RegressionTestExecutor {
|
|||
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
|
||||
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
|
||||
await HostDeleter.deleteHosts(null, metaHostName, zabbixAuthToken, cookie);
|
||||
await HostDeleter.deleteHosts(null, devHostNameWithTag, zabbixAuthToken, cookie);
|
||||
await HostDeleter.deleteHosts(null, devHostNameWithoutTag, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, regTemplateName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export class TemplateImporter {
|
|||
let templateNames = template.templates.map(t => t.name)
|
||||
let queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||
filter_host: templateNames
|
||||
}))
|
||||
}), ["templateid"])
|
||||
|
||||
if (isZabbixErrorResult(queryResult)) {
|
||||
let errorMessage = queryResult.error.message;
|
||||
|
|
|
|||
|
|
@ -642,12 +642,6 @@ export interface MutationImportUserRightsArgs {
|
|||
}
|
||||
|
||||
|
||||
export interface MutationRunAllRegressionTestsArgs {
|
||||
groupName: Scalars['String']['input'];
|
||||
hostName: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
|
||||
export interface MutationRunSmoketestArgs {
|
||||
groupName: Scalars['String']['input'];
|
||||
hostName: Scalars['String']['input'];
|
||||
|
|
@ -1597,7 +1591,7 @@ export type MutationResolvers<ContextType = any, ParentType extends ResolversPar
|
|||
importTemplateGroups?: Resolver<Maybe<Array<ResolversTypes['CreateTemplateGroupResponse']>>, ParentType, ContextType, RequireFields<MutationImportTemplateGroupsArgs, 'templateGroups'>>;
|
||||
importTemplates?: Resolver<Maybe<Array<ResolversTypes['ImportTemplateResponse']>>, ParentType, ContextType, RequireFields<MutationImportTemplatesArgs, 'templates'>>;
|
||||
importUserRights?: Resolver<Maybe<ResolversTypes['ImportUserRightsResult']>, ParentType, ContextType, RequireFields<MutationImportUserRightsArgs, 'dryRun' | 'input'>>;
|
||||
runAllRegressionTests?: Resolver<ResolversTypes['SmoketestResponse'], ParentType, ContextType, RequireFields<MutationRunAllRegressionTestsArgs, 'groupName' | 'hostName'>>;
|
||||
runAllRegressionTests?: Resolver<ResolversTypes['SmoketestResponse'], ParentType, ContextType>;
|
||||
runSmoketest?: Resolver<ResolversTypes['SmoketestResponse'], ParentType, ContextType, RequireFields<MutationRunSmoketestArgs, 'groupName' | 'hostName' | 'templateName'>>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ describe("Host and HostGroup Resolvers", () => {
|
|||
}));
|
||||
});
|
||||
|
||||
test("allDevices query", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1" }];
|
||||
test("allDevices query - with hostid", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1", deviceType: "GenericDevice" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = { hostids: 2 };
|
||||
|
|
@ -74,7 +74,63 @@ describe("Host and HostGroup Resolvers", () => {
|
|||
body: expect.objectContaining({
|
||||
method: "host.get",
|
||||
params: expect.objectContaining({
|
||||
hostids: 2
|
||||
hostids: 2,
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 4
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices query - with deviceType filter", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1", deviceType: "SomeType" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = { tag_deviceType: ["SomeType"] };
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
|
||||
const result = await resolvers.Query.allDevices(null, args, context);
|
||||
|
||||
expect(result).toEqual(mockDevices);
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith("host.get.with_items", expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 1,
|
||||
value: "SomeType"
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices query - ensures deviceType exists if no filter provided", async () => {
|
||||
const mockDevices = [{ hostid: "3", host: "Device with tag", deviceType: "SomeType" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
|
||||
const result = await resolvers.Query.allDevices(null, args, context);
|
||||
|
||||
expect(result).toEqual(mockDevices);
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith("host.get.with_items", expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
method: "host.get",
|
||||
params: expect.objectContaining({
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 4
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -163,6 +163,37 @@ describe("Query Optimization", () => {
|
|||
expect(callParams.output).toContain("items");
|
||||
});
|
||||
|
||||
test("allHosts optimization - keep selectTags when deviceType requested", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args: QueryAllHostsArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'hostid' } },
|
||||
{ kind: 'Field', name: { value: 'deviceType' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allHosts(null, args, context, info);
|
||||
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
output: ["hostid"],
|
||||
selectTags: expect.any(Array)
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices optimization - skip items when not requested", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
|
|
|
|||
173
src/test/schema_dependent_queries.test.ts
Normal file
173
src/test/schema_dependent_queries.test.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import {ApolloServer} from '@apollo/server';
|
||||
import {schema_loader} from '../api/schema.js';
|
||||
import {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||
import {Config} from "../common_utils.js";
|
||||
|
||||
describe("Schema-dependent Queries Integration Tests", () => {
|
||||
let server: ApolloServer;
|
||||
let postSpy: jest.SpyInstance;
|
||||
|
||||
let originalSchemas: any;
|
||||
let originalResolvers: any;
|
||||
let originalApiVersion: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
originalSchemas = Config.ADDITIONAL_SCHEMAS;
|
||||
originalResolvers = Config.ADDITIONAL_RESOLVERS;
|
||||
originalApiVersion = Config.API_VERSION;
|
||||
|
||||
// We need to bypass the static readonly nature of Config for this test.
|
||||
Object.defineProperty(Config, 'ADDITIONAL_SCHEMAS', {
|
||||
value: "./samples/extensions/location_tracker_devices.graphql,./samples/extensions/location_tracker_commons.graphql,./samples/extensions/display_devices.graphql",
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Config, 'ADDITIONAL_RESOLVERS', {
|
||||
value: "DistanceTrackerDevice,SinglePanelDevice",
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Config, 'API_VERSION', {
|
||||
value: "1.2.3",
|
||||
configurable: true
|
||||
});
|
||||
|
||||
const schema = await schema_loader();
|
||||
server = new ApolloServer({
|
||||
schema,
|
||||
});
|
||||
|
||||
postSpy = jest.spyOn(zabbixAPI, 'post');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
postSpy.mockRestore();
|
||||
Object.defineProperty(Config, 'ADDITIONAL_SCHEMAS', { value: originalSchemas });
|
||||
Object.defineProperty(Config, 'ADDITIONAL_RESOLVERS', { value: originalResolvers });
|
||||
Object.defineProperty(Config, 'API_VERSION', { value: originalApiVersion });
|
||||
});
|
||||
|
||||
test("TC-SCHEMA-01: DistanceTrackerDevice Comprehensive Query", async () => {
|
||||
const filePath = join(process.cwd(), 'docs', 'queries', 'sample_distance_tracker_test_query.graphql');
|
||||
const content = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
|
||||
|
||||
const queryMatch = content.match(/```graphql\n([\s\S]*?)\n```/);
|
||||
if (!queryMatch) {
|
||||
throw new Error(`No graphql block found in sample query file`);
|
||||
}
|
||||
const query = queryMatch[1];
|
||||
|
||||
// Setup mock responses for Zabbix API
|
||||
postSpy.mockImplementation((method: string) => {
|
||||
if (method === 'apiinfo.version') return Promise.resolve("7.4.0");
|
||||
if (method.startsWith('hostgroup.get')) {
|
||||
return Promise.resolve([
|
||||
{ groupid: "1", name: "Roadwork/Devices/Tracker" }
|
||||
]);
|
||||
}
|
||||
if (method.startsWith('host.get')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
hostid: "10001",
|
||||
host: "TRACKER_01",
|
||||
name: "Distance Tracker 01",
|
||||
deviceType: "DistanceTrackerDevice", // Manually mapped because we mock post()
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "1", name: "Count", key_: "state.current.count", lastvalue: "5", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "2", name: "Time From", key_: "state.current.timeFrom", lastvalue: "2024-01-01T10:00:00Z", lastclock: 1704103200, value_type: "4" },
|
||||
{ itemid: "3", name: "Time Until", key_: "state.current.timeUntil", lastvalue: "2024-01-01T11:00:00Z", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10003",
|
||||
host: "TRACKER_02",
|
||||
name: "Distance Tracker 02",
|
||||
deviceType: "DistanceTrackerDevice",
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "10", name: "Count", key_: "state.current.count", lastvalue: "10", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "11", name: "Time From", key_: "state.current.timeFrom", lastvalue: "09:58:09", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10004",
|
||||
host: "TRACKER_03",
|
||||
name: "Distance Tracker 03",
|
||||
deviceType: "DistanceTrackerDevice",
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "20", name: "Count", key_: "state.current.count", lastvalue: "0", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "21", name: "Time From", key_: "state.current.timeFrom", lastvalue: "", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10002",
|
||||
host: "DISPLAY_01",
|
||||
name: "LED Display 01",
|
||||
deviceType: "SinglePanelDevice", // Manually mapped because we mock post()
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "SinglePanelDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "4", name: "Content", key_: "state.current.values.1.contentText", lastvalue: "Roadwork Ahead", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
}
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const response = await server.executeOperation({
|
||||
query: query,
|
||||
}, {
|
||||
contextValue: { zabbixAuthToken: 'test-token', dataSources: { zabbixAPI: zabbixAPI } }
|
||||
});
|
||||
|
||||
if (response.body.kind === 'single') {
|
||||
const result = response.body.singleResult;
|
||||
if (result.errors) {
|
||||
console.error(`Errors in query:`, JSON.stringify(result.errors, null, 2));
|
||||
}
|
||||
expect(result.errors).toBeUndefined();
|
||||
|
||||
const data = result.data as any;
|
||||
expect(data.apiVersion).toBe("1.2.3");
|
||||
expect(data.zabbixVersion).toBe("7.4.0");
|
||||
expect(data.allHostGroups).toHaveLength(1);
|
||||
expect(data.allDevices).toBeDefined();
|
||||
|
||||
// Verify DistanceTrackerDevice resolution
|
||||
const tracker = data.allDevices.find((d: any) => d.host === "TRACKER_01");
|
||||
expect(tracker.deviceType).toBe("DistanceTrackerDevice");
|
||||
expect(tracker.state.current.count).toBe(5);
|
||||
expect(tracker.state.current.timeFrom).toBe("2024-01-01T10:00:00Z");
|
||||
|
||||
const tracker02 = data.allDevices.find((d: any) => d.host === "TRACKER_02");
|
||||
expect(tracker02.state.current.count).toBe(10);
|
||||
expect(tracker02.state.current.timeFrom).toBe("09:58:09");
|
||||
|
||||
const tracker03 = data.allDevices.find((d: any) => d.host === "TRACKER_03");
|
||||
expect(tracker03.state.current.timeFrom).toBe("");
|
||||
|
||||
// Verify allHosts with fragments
|
||||
const trackerInHosts = data.allHosts.find((h: any) => h.host === "TRACKER_01");
|
||||
expect(trackerInHosts.state.current.count).toBe(5);
|
||||
|
||||
const displayInHosts = data.allHosts.find((h: any) => h.host === "DISPLAY_01");
|
||||
expect(displayInHosts.deviceType).toBe("SinglePanelDevice");
|
||||
} else {
|
||||
throw new Error(`Unexpected response kind: ${response.body.kind}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue