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$">
|
<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>
|
<envs>
|
||||||
<env name="ADDITIONAL_RESOLVERS" value="SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice" />
|
<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="DEBUG" value="device-control-center-api:*" />
|
||||||
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
||||||
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
|
<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>
|
||||||
<component name="ChangeListManager">
|
<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.">
|
<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$/.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/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$/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-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/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/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/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/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/host_query.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/host_query.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/query_optimization.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/query_optimization.test.ts" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
@ -27,13 +43,13 @@
|
||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="149" />
|
<option name="cachedIndexableFilesCount" value="163" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="license" />
|
<entry key="$PROJECT_DIR$" value="vlsv" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
|
@ -95,7 +111,7 @@
|
||||||
"go.import.settings.migrated": "true",
|
"go.import.settings.migrated": "true",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
"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.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
|
@ -107,8 +123,8 @@
|
||||||
"npm.copy-schema.executor": "Run",
|
"npm.copy-schema.executor": "Run",
|
||||||
"npm.prod.executor": "Run",
|
"npm.prod.executor": "Run",
|
||||||
"npm.test.executor": "Run",
|
"npm.test.executor": "Run",
|
||||||
"settings.editor.selected.configurable": "ml.llm.mcp",
|
"settings.editor.selected.configurable": "junie.mcp",
|
||||||
"settings.editor.splitter.proportion": "0.28812414",
|
"settings.editor.splitter.proportion": "0.23751687",
|
||||||
"to.speed.mode.migration.done": "true",
|
"to.speed.mode.migration.done": "true",
|
||||||
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
|
@ -116,16 +132,16 @@
|
||||||
}]]></component>
|
}]]></component>
|
||||||
<component name="RecapSpentCounter">
|
<component name="RecapSpentCounter">
|
||||||
<option name="endsOfQuotaMs" value="1772398800000" />
|
<option name="endsOfQuotaMs" value="1772398800000" />
|
||||||
<option name="spentUsd" value="0.01011225" />
|
<option name="spentUsd" value="0.0915201" />
|
||||||
</component>
|
</component>
|
||||||
<component name="RecapUselessUpdatesCounter">
|
<component name="RecapUselessUpdatesCounter">
|
||||||
<option name="suspendCountdown" value="8" />
|
<option name="suspendCountdown" value="0" />
|
||||||
</component>
|
</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<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\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\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\testdata\templates" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\src\test" />
|
<recent name="\\wsl.localhost\Ubuntu\home\ahilbig\git\vcr\zabbix-graphql-api\src\test" />
|
||||||
</key>
|
</key>
|
||||||
|
|
@ -153,7 +169,7 @@
|
||||||
<node-interpreter value="project" />
|
<node-interpreter value="project" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="ADDITIONAL_RESOLVERS" value="SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice" />
|
<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="DEBUG" value="device-control-center-api:*" />
|
||||||
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
<env name="ZABBIX_PRIVILEGE_ESCALATION_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
|
||||||
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
|
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
|
||||||
|
|
@ -185,11 +201,11 @@
|
||||||
</list>
|
</list>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
|
<item itemvalue="npm.prod" />
|
||||||
|
<item itemvalue="npm.copy-schema" />
|
||||||
|
<item itemvalue="npm.test" />
|
||||||
<item itemvalue="npm.test" />
|
<item itemvalue="npm.test" />
|
||||||
<item itemvalue="npm.prod" />
|
<item itemvalue="npm.prod" />
|
||||||
<item itemvalue="npm.copy-schema" />
|
|
||||||
<item itemvalue="npm.prod" />
|
|
||||||
<item itemvalue="npm.copy-schema" />
|
|
||||||
</list>
|
</list>
|
||||||
</recent_temporary>
|
</recent_temporary>
|
||||||
</component>
|
</component>
|
||||||
|
|
@ -488,7 +504,7 @@
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
<line-breakpoint enabled="true" type="javascript">
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
|
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
|
||||||
<line>213</line>
|
<line>253</line>
|
||||||
<option name="timeStamp" value="6" />
|
<option name="timeStamp" value="6" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
</breakpoints>
|
</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`
|
- *Reference*: `schema/mutations.graphql` (importHosts, importTemplates, importUserRights, etc.), `docs/sample_import_*.graphql`
|
||||||
|
|
||||||
- **Dynamic Schema Extension**: Extend the schema without code changes using environment variables
|
- **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
|
- **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`
|
- *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
|
HOST_TYPE_FILTER_DEFAULT=Roadwork/Devices
|
||||||
|
|
||||||
# Schema Extensions (No-Code)
|
# 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
|
ADDITIONAL_RESOLVERS=SinglePanelDevice,FourPanelDevice,DistanceTrackerDevice
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- SCHEMA_PATH=/usr/app/dist/schema/
|
- SCHEMA_PATH=/usr/app/dist/schema/
|
||||||
- ZABBIX_DEVELOPMENT_TOKEN=${ZABBIX_DEVELOPMENT_TOKEN}
|
- ZABBIX_DEVELOPMENT_TOKEN=${ZABBIX_DEVELOPMENT_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- ./samples:/usr/app/dist/samples
|
||||||
|
|
||||||
apollo-mcp-server:
|
apollo-mcp-server:
|
||||||
image: ghcr.io/apollographql/apollo-mcp-server:latest
|
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.
|
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
|
### 📋 Prerequisites
|
||||||
- Zabbix Template Group `Templates/Roadwork/Devices` exists.
|
- Zabbix Template Group `Templates/Roadwork/Devices` exists.
|
||||||
- Zabbix GraphQL API is running.
|
- Zabbix GraphQL API is running.
|
||||||
|
|
||||||
### 🛠️ Step 1: Define the Schema Extension
|
### 🛠️ 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.
|
> **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 {
|
type DistanceTrackerValues {
|
||||||
timeFrom: Time
|
timeFrom: String
|
||||||
timeUntil: Time
|
timeUntil: String
|
||||||
count: Int
|
count: Int
|
||||||
# The distances are modelled using a type which is already defined in location_tracker_commons.graphql
|
# The distances are modelled using a type which is already defined in location_tracker_commons.graphql
|
||||||
distances: [SensorDistanceValue!]
|
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
|
### ⚙️ Step 2: Configure Environment Variables
|
||||||
Add the new schema and resolver to your `.env` file:
|
Add the new schema and resolver to your `.env` file:
|
||||||
```env
|
```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
|
ADDITIONAL_RESOLVERS=DistanceTrackerDevice
|
||||||
```
|
```
|
||||||
Restart the API server.
|
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}`).
|
- The device has geo-coordinates set via user macros (e.g. `{$LAT}` and `{$LON}`).
|
||||||
|
|
||||||
### 🛠️ Step 1: Define the Schema Extension
|
### 🛠️ 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**
|
**Sample: Weather Sensor**
|
||||||
```graphql
|
```graphql
|
||||||
|
|
@ -262,7 +264,7 @@ type GroundValues {
|
||||||
### ⚙️ Step 2: Register the Resolver
|
### ⚙️ Step 2: Register the Resolver
|
||||||
Add the new types and schemas to your `.env` file to enable the dynamic resolver:
|
Add the new types and schemas to your `.env` file to enable the dynamic resolver:
|
||||||
```env
|
```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
|
ADDITIONAL_RESOLVERS=WeatherSensorDevice,GroundValueChecker
|
||||||
```
|
```
|
||||||
Restart the API server to apply the changes.
|
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
|
## 🍳 Recipe: Provisioning a New Host
|
||||||
|
|
||||||
### 📋 Prerequisites
|
### 📋 Prerequisites
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ The `GraphqlParamsToNeededZabbixOutput` class provides static methods to map Gra
|
||||||
### 3. Resolver Integration
|
### 3. Resolver Integration
|
||||||
Resolvers use the mapper to determine the required output and pass it to the datasource:
|
Resolvers use the mapper to determine the required output and pass it to the datasource:
|
||||||
```typescript
|
```typescript
|
||||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(info);
|
||||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
||||||
.executeRequestThrowError(dataSources.zabbixAPI, new ParsedArgs(args), output);
|
.executeRequestThrowError(dataSources.zabbixAPI, new ParsedArgs(args), output);
|
||||||
```
|
```
|
||||||
|
|
@ -30,6 +30,7 @@ return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
||||||
### 4. Indirect Dependencies
|
### 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:
|
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.
|
- **`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
|
## 🛠️ Configuration
|
||||||
Optimization rules are defined in the constructor of specialized `ZabbixRequest` classes.
|
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
|
### 📋 Supported Optimizations
|
||||||
- **Hosts & Devices**:
|
- **Hosts & Devices**:
|
||||||
- `selectParentTemplates` skipped if `parentTemplates` not requested.
|
- `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.
|
- `selectHostGroups` skipped if `hostgroups` not requested.
|
||||||
- `selectItems` skipped if `items` (or `state`) not requested.
|
- `selectItems` skipped if `items` (or `state`) not requested.
|
||||||
- `selectInventory` skipped if `inventory` 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)
|
- `zabbix.graphql` - Zabbix-specific types (see detailed documentation in file comments)
|
||||||
- `device_value_commons.graphql` - Common value 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)
|
- `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.
|
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:
|
Extend the schema without code changes using environment variables:
|
||||||
|
|
||||||
```bash
|
```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
|
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.
|
- [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.
|
- [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.
|
- [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
|
### 📄 Templates
|
||||||
- [Query Templates](./sample_templates_query.graphql): List available templates and their items.
|
- [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-DOCS-01**: Validate all Zabbix documentation sample queries.
|
||||||
- **TC-MCP-01**: Validate all MCP operation files against the schema.
|
- **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
|
### 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-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.
|
- **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.
|
- **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).
|
- **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.
|
- **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
|
## ✅ 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-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-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-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-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) |
|
| 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.
|
# Runs all regression tests.
|
||||||
# Variables: hostName, groupName
|
mutation RunAllRegressionTests {
|
||||||
mutation RunAllRegressionTests($hostName: String!, $groupName: String!) {
|
runAllRegressionTests {
|
||||||
runAllRegressionTests(hostName: $hostName, groupName: $groupName) {
|
|
||||||
success
|
success
|
||||||
message
|
message
|
||||||
steps {
|
steps {
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,11 @@ type DistanceTrackerValues {
|
||||||
"""
|
"""
|
||||||
Start of time interval for the delivered device counting value.
|
Start of time interval for the delivered device counting value.
|
||||||
"""
|
"""
|
||||||
timeFrom: Time
|
timeFrom: String
|
||||||
"""
|
"""
|
||||||
End of time interval for the delivered device counting value.
|
End of time interval for the delivered device counting value.
|
||||||
"""
|
"""
|
||||||
timeUntil: Time
|
timeUntil: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Number of unique device keys detected between timeFrom and timeUntil.
|
Number of unique device keys detected between timeFrom and timeUntil.
|
||||||
|
|
@ -142,12 +142,7 @@ type Mutation {
|
||||||
"""
|
"""
|
||||||
Runs all regression tests.
|
Runs all regression tests.
|
||||||
"""
|
"""
|
||||||
runAllRegressionTests(
|
runAllRegressionTests: SmoketestResponse!
|
||||||
"""Technical name for the test host."""
|
|
||||||
hostName: String!,
|
|
||||||
"""Technical name for the test host group."""
|
|
||||||
groupName: String!
|
|
||||||
): SmoketestResponse!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export function createResolvers(): Resolvers {
|
||||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||||
args.tag_hostType ??= [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)
|
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(
|
.executeRequestThrowError(
|
||||||
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
|
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
|
||||||
|
|
@ -120,7 +120,7 @@ export function createResolvers(): Resolvers {
|
||||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||||
args.tag_hostType ??= [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)
|
return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(
|
.executeRequestThrowError(
|
||||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
|
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
|
||||||
|
|
@ -133,7 +133,7 @@ export function createResolvers(): Resolvers {
|
||||||
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
||||||
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(
|
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
|
||||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
|
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
|
||||||
)
|
)
|
||||||
|
|
@ -173,7 +173,7 @@ export function createResolvers(): Resolvers {
|
||||||
name: args.name_pattern
|
name: args.name_pattern
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
|
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
|
||||||
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
||||||
},
|
},
|
||||||
|
|
@ -287,11 +287,11 @@ export function createResolvers(): Resolvers {
|
||||||
}: any) => {
|
}: any) => {
|
||||||
return SmoketestExecutor.runSmoketest(args.hostName, args.templateName, args.groupName, zabbixAuthToken, cookie)
|
return SmoketestExecutor.runSmoketest(args.hostName, args.templateName, args.groupName, zabbixAuthToken, cookie)
|
||||||
},
|
},
|
||||||
runAllRegressionTests: async (_parent: any, args: any, {
|
runAllRegressionTests: async (_parent: any, _args: any, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie
|
cookie
|
||||||
}: any) => {
|
}: any) => {
|
||||||
return RegressionTestExecutor.runAllRegressionTests(args.hostName, args.groupName, zabbixAuthToken, cookie)
|
return RegressionTestExecutor.runAllRegressionTests(zabbixAuthToken, cookie)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,20 @@
|
||||||
import {GraphQLResolveInfo} from "graphql";
|
import {GraphQLResolveInfo} from "graphql";
|
||||||
import {getRequestedFields} from "../api/graphql_utils.js";
|
import {getRequestedFields} from "../api/graphql_utils.js";
|
||||||
import {
|
|
||||||
QueryAllDevicesArgs,
|
|
||||||
QueryAllHostGroupsArgs,
|
|
||||||
QueryAllHostsArgs,
|
|
||||||
QueryTemplatesArgs
|
|
||||||
} from "../schema/generated/graphql.js";
|
|
||||||
|
|
||||||
export class GraphqlParamsToNeededZabbixOutput {
|
export class GraphqlParamsToNeededZabbixOutput {
|
||||||
static mapAllHosts(args: QueryAllHostsArgs, info: GraphQLResolveInfo): string[] {
|
static mapAllHosts(info: GraphQLResolveInfo): string[] {
|
||||||
return getRequestedFields(info);
|
return getRequestedFields(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static mapAllDevices(args: QueryAllDevicesArgs, info: GraphQLResolveInfo): string[] {
|
static mapAllDevices(info: GraphQLResolveInfo): string[] {
|
||||||
return getRequestedFields(info);
|
return getRequestedFields(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static mapAllHostGroups(args: QueryAllHostGroupsArgs, info: GraphQLResolveInfo): string[] {
|
static mapAllHostGroups(info: GraphQLResolveInfo): string[] {
|
||||||
return getRequestedFields(info);
|
return getRequestedFields(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static mapTemplates(args: QueryTemplatesArgs, info: GraphQLResolveInfo): string[] {
|
static mapTemplates(info: GraphQLResolveInfo): string[] {
|
||||||
return getRequestedFields(info);
|
return getRequestedFields(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
||||||
this.skippableZabbixParams.set("selectTags", "tags");
|
this.skippableZabbixParams.set("selectTags", "tags");
|
||||||
this.skippableZabbixParams.set("selectInheritedTags", "tags");
|
this.skippableZabbixParams.set("selectInheritedTags", "tags");
|
||||||
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
|
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
|
||||||
|
this.impliedFields.set("deviceType", ["tags"]);
|
||||||
|
this.impliedFields.set("hostType", ["tags"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
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)
|
// Use name_pattern which now searches both visibility name and technical name (host)
|
||||||
let templates = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ParsedArgs({
|
let templates = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ParsedArgs({
|
||||||
name_pattern: templateName
|
name_pattern: templateName
|
||||||
}))
|
}), ["templateid", "host"])
|
||||||
|
|
||||||
if (isZabbixErrorResult(templates) || !templates?.length) {
|
if (isZabbixErrorResult(templates) || !templates?.length) {
|
||||||
logger.error(`Unable to find templateName=${templateName}`)
|
logger.error(`Unable to find templateName=${templateName}`)
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ export class HostImporter {
|
||||||
{
|
{
|
||||||
tag_deviceType: deviceType
|
tag_deviceType: deviceType
|
||||||
}
|
}
|
||||||
));
|
), ["templateid"]);
|
||||||
|
|
||||||
if (templates?.length) {
|
if (templates?.length) {
|
||||||
result = Number(templates[0].templateid)
|
result = Number(templates[0].templateid)
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,16 @@ import {
|
||||||
ZabbixQueryHostsGenericRequestWithItems
|
ZabbixQueryHostsGenericRequestWithItems
|
||||||
} from "../datasources/zabbix-hosts.js";
|
} from "../datasources/zabbix-hosts.js";
|
||||||
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.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 {
|
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[] = [];
|
const steps: SmoketestStep[] = [];
|
||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
|
const hostName = "REG_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Regression 1: Locations query argument order
|
// Regression 1: Locations query argument order
|
||||||
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
|
// 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;
|
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) {
|
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) {
|
} catch (error) {
|
||||||
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||||
|
|
@ -469,6 +479,57 @@ export class RegressionTestExecutor {
|
||||||
});
|
});
|
||||||
if (!optNegSuccess) success = false;
|
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)
|
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
||||||
const groupResult = await HostImporter.importHostGroups([{
|
const groupResult = await HostImporter.importHostGroups([{
|
||||||
groupName: groupName
|
groupName: groupName
|
||||||
|
|
@ -486,6 +547,8 @@ export class RegressionTestExecutor {
|
||||||
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
|
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
|
||||||
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
|
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
|
||||||
await HostDeleter.deleteHosts(null, metaHostName, 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, regTemplateName, zabbixAuthToken, cookie);
|
||||||
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
||||||
await TemplateDeleter.deleteTemplates(null, macroTemplateName, 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 templateNames = template.templates.map(t => t.name)
|
||||||
let queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
let queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||||
filter_host: templateNames
|
filter_host: templateNames
|
||||||
}))
|
}), ["templateid"])
|
||||||
|
|
||||||
if (isZabbixErrorResult(queryResult)) {
|
if (isZabbixErrorResult(queryResult)) {
|
||||||
let errorMessage = queryResult.error.message;
|
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 {
|
export interface MutationRunSmoketestArgs {
|
||||||
groupName: Scalars['String']['input'];
|
groupName: Scalars['String']['input'];
|
||||||
hostName: 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'>>;
|
importTemplateGroups?: Resolver<Maybe<Array<ResolversTypes['CreateTemplateGroupResponse']>>, ParentType, ContextType, RequireFields<MutationImportTemplateGroupsArgs, 'templateGroups'>>;
|
||||||
importTemplates?: Resolver<Maybe<Array<ResolversTypes['ImportTemplateResponse']>>, ParentType, ContextType, RequireFields<MutationImportTemplatesArgs, 'templates'>>;
|
importTemplates?: Resolver<Maybe<Array<ResolversTypes['ImportTemplateResponse']>>, ParentType, ContextType, RequireFields<MutationImportTemplatesArgs, 'templates'>>;
|
||||||
importUserRights?: Resolver<Maybe<ResolversTypes['ImportUserRightsResult']>, ParentType, ContextType, RequireFields<MutationImportUserRightsArgs, 'dryRun' | 'input'>>;
|
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'>>;
|
runSmoketest?: Resolver<ResolversTypes['SmoketestResponse'], ParentType, ContextType, RequireFields<MutationRunSmoketestArgs, 'groupName' | 'hostName' | 'templateName'>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ describe("Host and HostGroup Resolvers", () => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allDevices query", async () => {
|
test("allDevices query - with hostid", async () => {
|
||||||
const mockDevices = [{ hostid: "2", host: "Device 1" }];
|
const mockDevices = [{ hostid: "2", host: "Device 1", deviceType: "GenericDevice" }];
|
||||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||||
|
|
||||||
const args: QueryAllDevicesArgs = { hostids: 2 };
|
const args: QueryAllDevicesArgs = { hostids: 2 };
|
||||||
|
|
@ -74,7 +74,63 @@ describe("Host and HostGroup Resolvers", () => {
|
||||||
body: expect.objectContaining({
|
body: expect.objectContaining({
|
||||||
method: "host.get",
|
method: "host.get",
|
||||||
params: expect.objectContaining({
|
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");
|
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 () => {
|
test("allDevices optimization - skip items when not requested", async () => {
|
||||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
(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