feat(query-optimization): implement GraphQL query optimization and enhance regression suite

- **Optimization**: Implemented automatic Zabbix parameter optimization by analyzing GraphQL selection sets.

- **ZabbixRequest**: Added optimizeZabbixParams with support for skippable parameters and implied field dependencies (e.g., state -> items).

- **Resolvers**: Updated allHosts, allDevices, allHostGroups, and templates to pass requested fields to data sources.

- **Data Sources**: Optimized ZabbixQueryHostsGenericRequest and ZabbixQueryTemplatesRequest to skip unnecessary Zabbix API calls.

- **Regression Tests**: Enhanced RegressionTestExecutor with new tests for optimization (REG-OPT, REG-OPT-NEG), state retrieval (REG-STATE), dependent items (REG-DEP), and empty results (REG-EMPTY).

- **Documentation**: Created query_optimization.md How-To guide and updated roadmap.md, README.md, and tests.md.

- **Bug Fixes**: Fixed deviceType tag assignment during host import and corrected ZabbixCreateHostRequest to support tags.
This commit is contained in:
Andreas Hilbig 2026-02-02 06:23:35 +01:00
parent ad104acde2
commit 97a0f70fd6
16 changed files with 835 additions and 69 deletions

View file

@ -7,6 +7,9 @@ This directory contains detailed guides on how to use and extend the Zabbix Grap
### 🍳 [Cookbook](./cookbook.md) ### 🍳 [Cookbook](./cookbook.md)
Practical, step-by-step recipes for common tasks, designed for both humans and AI-based test generation. Practical, step-by-step recipes for common tasks, designed for both humans and AI-based test generation.
### ⚡ [Query Optimization](./query_optimization.md)
Learn how the API optimizes Zabbix requests by reducing output fields and skipping unnecessary parameters based on the GraphQL query.
### 📊 [Schema and Schema Extension](./schema.md) ### 📊 [Schema and Schema Extension](./schema.md)
Learn about the GraphQL schema structure, how Zabbix entities map to GraphQL types, and how to use the dynamic schema extension system. Learn about the GraphQL schema structure, how Zabbix entities map to GraphQL types, and how to use the dynamic schema extension system.

View file

@ -0,0 +1,51 @@
# ⚡ Query Optimization
This document describes how the Zabbix GraphQL API optimizes queries to reduce data fetching from Zabbix, improving performance and reducing network load.
## 🚀 Overview
The optimization works by analyzing the requested GraphQL fields and only fetching the necessary data from the Zabbix API. This is achieved through:
- **Output Reduction**: Dynamically setting the `output` parameter in Zabbix requests.
- **Parameter Skipping**: Automatically removing optional Zabbix parameters (like `selectTags` or `selectItems`) if the corresponding GraphQL fields are not requested.
## 🏗️ Implementation Details
### 1. `ZabbixRequest` Enhancement
The base `ZabbixRequest` class handles the core optimization logic:
- `optimizeZabbixParams(params, output)`: This method modifies the Zabbix parameters. It filters the `output` array to match the requested fields and removes "skippable" parameters based on rules.
- `skippableZabbixParams`: A map that defines dependencies between Zabbix parameters and GraphQL fields.
- *Example*: `this.skippableZabbixParams.set("selectTags", "tags")` means `selectTags` will be removed if `tags` is not in the requested output.
### 2. Parameter Mapping
The `GraphqlParamsToNeededZabbixOutput` class provides static methods to map GraphQL query arguments and the selection set (`GraphQLResolveInfo`) to a list of needed Zabbix fields.
### 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);
return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
.executeRequestThrowError(dataSources.zabbixAPI, new ParsedArgs(args), output);
```
### 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.
## 🛠️ Configuration
Optimization rules are defined in the constructor of specialized `ZabbixRequest` classes.
### 📋 Supported Optimizations
- **Hosts & Devices**:
- `selectParentTemplates` skipped if `parentTemplates` not requested.
- `selectTags` and `selectInheritedTags` skipped if `tags` not requested.
- `selectHostGroups` skipped if `hostgroups` not requested.
- `selectItems` skipped if `items` (or `state`) not requested.
- `selectInventory` skipped if `inventory` not requested.
- **Templates**:
- `selectItems` skipped if `items` not requested.
## ✅ Verification
You can verify that optimization is working by:
1. Enabling `debug` log level (`LOG_LEVEL=debug`).
2. Observing the Zabbix request bodies in the logs.
3. Checking that the `output` array is minimized and `select*` parameters are omitted when not needed.

View file

@ -53,6 +53,11 @@ This document outlines the test cases and coverage for the Zabbix GraphQL API.
- **TC-AUTH-04**: Import user rights. - **TC-AUTH-04**: Import user rights.
- **TC-AUTH-05**: Import user rights using sample mutation. - **TC-AUTH-05**: Import user rights using sample mutation.
### Query Optimization
- **TC-OPT-01**: Verify that GraphQL queries only fetch requested fields from Zabbix (reduced output).
- **TC-OPT-02**: Verify that skippable Zabbix parameters (like selectItems) are omitted if not requested in GraphQL.
- **TC-OPT-03**: Verify that indirect dependencies (e.g., `state` requiring `items`) are correctly handled by the optimization logic.
### System and Configuration ### System and Configuration
- **TC-CONF-01**: Schema loader uses Config variables. - **TC-CONF-01**: Schema loader uses Config variables.
- **TC-CONF-02**: Zabbix API constants derived from Config. - **TC-CONF-02**: Zabbix API constants derived from Config.
@ -77,6 +82,11 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
- **Template technical name lookup**: Verifies that templates can be correctly identified by their technical name (`host` field) when linking them to hosts during import. - **Template technical name lookup**: Verifies that templates can be correctly identified by their technical name (`host` field) when linking them to hosts during import.
- **HTTP Agent URL support**: Verifies that templates containing HTTP Agent items with a configured URL can be imported successfully (verifying the addition of the `url` field to `CreateTemplateItem`). - **HTTP Agent URL support**: Verifies that templates containing HTTP Agent items with a configured URL can be imported successfully (verifying the addition of the `url` field to `CreateTemplateItem`).
- **Host retrieval and visibility**: Verifies that newly imported hosts are immediately visible and retrievable via the `allHosts` query, including correctly delivered assigned templates and assigned host groups (verifying the fix for `output` fields in the host query data source). - **Host retrieval and visibility**: Verifies that newly imported hosts are immediately visible and retrievable via the `allHosts` query, including correctly delivered assigned templates and assigned host groups (verifying the fix for `output` fields in the host query data source).
- **Query Optimization**: Verifies that GraphQL requests correctly translate into optimized Zabbix parameters, reducing the amount of data fetched (verifying the query optimization feature).
- **Empty result handling**: Verifies that queries return an empty array instead of an error when no entities match the provided filters.
- **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.
## ✅ Test Coverage Checklist ## ✅ Test Coverage Checklist
@ -116,6 +126,9 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
| TC-AUTH-03 | hasPermissions query | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) | | TC-AUTH-03 | hasPermissions query | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
| TC-AUTH-04 | importUserRights mutation | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) | | TC-AUTH-04 | importUserRights mutation | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
| TC-AUTH-05 | Import user rights using sample | Integration | Jest | [src/test/user_rights_integration.test.ts](../src/test/user_rights_integration.test.ts) | | TC-AUTH-05 | Import user rights using sample | Integration | Jest | [src/test/user_rights_integration.test.ts](../src/test/user_rights_integration.test.ts) |
| TC-OPT-01 | Verify Query Optimization (reduced output) | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
| TC-OPT-02 | Verify skippable parameters | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
| TC-OPT-03 | Verify indirect dependencies | Unit | Jest | [src/test/indirect_dependencies.test.ts](../src/test/indirect_dependencies.test.ts) |
| TC-CONF-01 | schema_loader uses Config variables | Unit | Jest | [src/test/schema_config.test.ts](../src/test/schema_config.test.ts) | | TC-CONF-01 | schema_loader uses Config variables | Unit | Jest | [src/test/schema_config.test.ts](../src/test/schema_config.test.ts) |
| TC-CONF-02 | constants are derived from Config | Unit | Jest | [src/test/zabbix_api_config.test.ts](../src/test/zabbix_api_config.test.ts) | | TC-CONF-02 | constants are derived from Config | Unit | Jest | [src/test/zabbix_api_config.test.ts](../src/test/zabbix_api_config.test.ts) |
| TC-CONF-03 | logger levels initialized from Config | Unit | Jest | [src/test/logger_config.test.ts](../src/test/logger_config.test.ts) | | TC-CONF-03 | logger levels initialized from Config | Unit | Jest | [src/test/logger_config.test.ts](../src/test/logger_config.test.ts) |

View file

@ -0,0 +1,69 @@
# 🏗️ Trade Fair Logistics Requirements
This document outlines the requirements for extending the Zabbix GraphQL API to support trade fair logistics, derived from the analysis of the "KI-gestützte Orchestrierung in der Messelogistik" (AI-supported orchestration in trade fair logistics) pilot at Koelnmesse.
## 📋 Project Context
The goal is to use the **Virtual Control Room (VCR)** as an orchestration platform to improve punctuality, throughput, and exception handling in trade fair logistics.
## 🛠️ Key Use Cases
- **Slot Risk Scoring & Proactive Rescheduling**:
- *Description*: Proactive detection of missed delivery slots using ETAs and historical data.
- *AI Function*: Calculates slot-miss risk and suggests next best actions (e.g. shift slot, alternative gate).
- *Zabbix Role*: Monitoring ETA vs. Slot time, triggering alerts on high risk.
- **Exception Copilot for Dispatch & Gate**:
- *Description*: Standardized workflows (Playbooks) for managing arrival deviations (e.g. no slot, wrong gate, missing documents).
- *AI Function*: Classifies exceptions and provides communication templates.
- *Zabbix Role*: Capturing exception events as triggers and managing the resolution state.
- **Multilingual Driver Assistant**:
- *Description*: Step-by-step instructions for drivers on-site to reduce misunderstandings and wrong turns.
- *Zabbix Role*: Providing real-time status updates (e.g. "Gate 4 is ready for you") to external communication interfaces.
- **Handling Readiness (Stapler/Personal/Rampe)**:
- *Description*: Coordinating truck arrivals with the availability of handling resources like forklifts and ramps.
- *Zabbix Role*: Monitoring the status and capacity of logistics assets and personnel.
- **VCR Setup Copilot**:
- *Description*: Template-based configuration to scale the VCR for different venues (e.g. Koelnmesse, Düsseldorf) and varying event rules.
- *Zabbix Role*: Management of venue-specific and event-specific host groups and templates.
## ⚙️ Technical Requirements for the API
- **Dynamic Device Modeling**:
- Support for complex **Delivery** entities as Zabbix hosts.
- Inclusion of dynamic attributes such as Slot-ID, ETA, and Gate assignments.
- **Hierarchical Data Mapping**:
- Mapping nested logistics data (e.g. cargo details, handling status) to hierarchical Zabbix item structures.
- Use of tags for classification and filtering of logistics tasks.
- **Real-time Telemetry Integration**:
- High-frequency ingestion of GPS and sensor data (e.g. temperature, shock) from mobile tracking devices.
- Support for Zabbix trapper items to receive external push updates.
- **AI-Integration Hooks**:
- Enable external AI systems to push "Risk Scores" and "Next Best Actions" into Zabbix.
- Use of Zabbix triggers to orchestrate AI-driven suggestions.
- **Workflow Orchestration**:
- Ability to trigger external actions (e.g. sending notifications to drivers, creating tickets) based on Zabbix triggers.
- Integration with the Model Context Protocol (MCP) to allow AI agents to manage logistics exceptions.
- **Multi-Venue Templates**:
- Provision of reusable template structures for different exhibition centers and recurring events.
- Support for bulk import/export of venue-specific configurations.
## ✅ KPIs for Success Measurement
- **Slot Hit Rate**: Percentage of vehicles arriving within their booked time window.
- **P22-Quote**: Frequency of vehicles needing to be redirected to waiting areas (P22).
- **Gate Waiting Time**: Average time from arrival at the venue to successful check-in.
- **Throughput**: Number of vehicles processed per gate/hour.
- **Average Handle Time (AHT)**: Mean time to resolve a logistics exception/ticket.
- **First Contact Resolution**: Rate of exceptions resolved without further escalation.
## 🔗 References
- **Analysis Document**: [docs/KI für Event- und Messelogistik.pdf](../KI%20f%C3%BCr%20Event-%20und%20Messelogistik.pdf)
- **Roadmap**: [roadmap.md](../../roadmap.md)

View file

@ -3,21 +3,26 @@
This document outlines the achieved milestones and planned enhancements for the Zabbix GraphQL API project. This document outlines the achieved milestones and planned enhancements for the Zabbix GraphQL API project.
## ✅ Achieved Milestones ## ✅ Achieved Milestones
- **🎯 VCR Product Integration**: - **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
- Developed a specialized **GraphQL API** as part of the VCR Product. This enables the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**. - *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
- **🔓 Open Source Extraction & AI Integration**: - **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
- Extracted the core functionality of the API to publish it as an **Open Source** project. - *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
- Enhanced it with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases within the VCR or other applications.
## 📅 Planned Enhancements ## 📅 Planned Enhancements
- **📦 CI/CD & Package Publishing**: - **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
- Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
- **🧱 Flexible Usage by publishing to [npmjs.com](https://www.npmjs.com/)**: - **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
- Transform the package into a versatile tool that can be used either **standalone** or as a **core library**, allowing other projects to include and extend it easily. - *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
- *Simulation*:
- Create mocked "real world sensor devices" relevant for the use case.
- Create a sample device collecting relevant information from public APIs, e.g. weather conditions or traffic conditions at a given location.
- Simulate the traffic conditions on the route by using the simulated sensor devices.
- *Configuration*: Analyze a real-world transport and configure Zabbix by placing sensor devices at the right places of the route.
- **🧩 Extension Project**: `zabbix-graphql-api-problems` - **📦 CI/CD & Package Publishing**: Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
- Create the first official extension, `zabbix-graphql-api-problems`
- Add support for **problem and trigger-related** queries. This will leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows. - **🧱 Flexible Usage**: Transform the package into a versatile tool that can be used either **standalone** or as a **core library** (published to [npmjs.com](https://www.npmjs.com/)).
- **🧩 Extension Project**: Add support for **problem and trigger-related** queries through the `zabbix-graphql-api-problems` extension.
- *AI Integration*: Leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows.

31
src/api/graphql_utils.ts Normal file
View file

@ -0,0 +1,31 @@
import {GraphQLResolveInfo, FieldNode, FragmentDefinitionNode, InlineFragmentNode} from "graphql";
export function getRequestedFields(info: GraphQLResolveInfo): string[] {
if (!info || !info.fieldNodes) return [];
const fields: string[] = [];
const fieldNode = info.fieldNodes[0];
function extractFields(selectionSet: any, prefix: string = "") {
if (!selectionSet) return;
for (const selection of selectionSet.selections) {
if (selection.kind === 'Field') {
const fieldName = (selection as FieldNode).name.value;
const fullPath = prefix ? `${prefix}.${fieldName}` : fieldName;
fields.push(fullPath);
if (selection.selectionSet) {
extractFields(selection.selectionSet, fullPath);
}
} else if (selection.kind === 'InlineFragment') {
extractFields((selection as InlineFragmentNode).selectionSet, prefix);
} else if (selection.kind === 'FragmentSpread') {
const fragment = info.fragments[selection.name.value];
if (fragment) {
extractFields(fragment.selectionSet, prefix);
}
}
}
}
extractFields(fieldNode.selectionSet);
return fields;
}

View file

@ -62,6 +62,7 @@ import {GraphQLInterfaceType, GraphQLList} from "graphql/type/index.js";
import {isDevice} from "./resolver_helpers.js"; import {isDevice} from "./resolver_helpers.js";
import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js"; import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js";
import {Config} from "../common_utils.js"; import {Config} from "../common_utils.js";
import {GraphqlParamsToNeededZabbixOutput} from "../datasources/graphql-params-to-zabbix-output.js";
export function createResolvers(): Resolvers { export function createResolvers(): Resolvers {
@ -102,36 +103,39 @@ export function createResolvers(): Resolvers {
allHosts: async (_parent: any, args: QueryAllHostsArgs, { allHosts: async (_parent: any, args: QueryAllHostsArgs, {
zabbixAuthToken, zabbixAuthToken,
cookie, dataSources cookie, dataSources
}: any) => { }: any, info: any) => {
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);
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie) return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
.executeRequestThrowError( .executeRequestThrowError(
dataSources.zabbixAPI, new ParsedArgs(args) dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
) )
}, },
allDevices: async (_parent: any, args: QueryAllDevicesArgs, { allDevices: async (_parent: any, args: QueryAllDevicesArgs, {
zabbixAuthToken, zabbixAuthToken,
cookie, dataSources cookie, dataSources
}: any) => { }: any, info: any) => {
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);
return await new ZabbixQueryDevices(zabbixAuthToken, cookie) return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
.executeRequestThrowError( .executeRequestThrowError(
dataSources.zabbixAPI, new ZabbixQueryDevicesArgs(args) dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
) )
}, },
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, { allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
zabbixAuthToken, zabbixAuthToken,
cookie cookie, dataSources
}: any) => { }: any, info: any) => {
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);
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError( return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
zabbixAPI, new ZabbixQueryHostgroupsParams(args) dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
) )
}, },
@ -158,8 +162,8 @@ export function createResolvers(): Resolvers {
templates: async (_parent: any, args: QueryTemplatesArgs, { templates: async (_parent: any, args: QueryTemplatesArgs, {
zabbixAuthToken, zabbixAuthToken,
cookie cookie, dataSources
}: any) => { }: any, info: any) => {
let params: any = {} let params: any = {}
if (args.hostids) { if (args.hostids) {
params.templateids = args.hostids params.templateids = args.hostids
@ -169,8 +173,9 @@ export function createResolvers(): Resolvers {
name: args.name_pattern name: args.name_pattern
} }
} }
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie) return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params)); .executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
}, },
allTemplateGroups: async (_parent: any, args: any, { allTemplateGroups: async (_parent: any, args: any, {

View file

@ -0,0 +1,26 @@
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[] {
return getRequestedFields(info);
}
static mapAllDevices(args: QueryAllDevicesArgs, info: GraphQLResolveInfo): string[] {
return getRequestedFields(info);
}
static mapAllHostGroups(args: QueryAllHostGroupsArgs, info: GraphQLResolveInfo): string[] {
return getRequestedFields(info);
}
static mapTemplates(args: QueryTemplatesArgs, info: GraphQLResolveInfo): string[] {
return getRequestedFields(info);
}
}

View file

@ -80,12 +80,12 @@ export class ZabbixAPI
return super.post(path, request); return super.post(path, request);
} }
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true): Promise<T | ZabbixErrorResult> { async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args); return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
} }
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true) { async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true, output?: string[]) {
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError); return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError, output);
} }
async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) { async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) {

View file

@ -17,10 +17,14 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
constructor(path: string, authToken?: string | null, cookie?: string | null) { constructor(path: string, authToken?: string | null, cookie?: string | null) {
super(path, authToken, cookie); super(path, authToken, cookie);
this.skippableZabbixParams.set("selectParentTemplates", "parentTemplates");
this.skippableZabbixParams.set("selectTags", "tags");
this.skippableZabbixParams.set("selectInheritedTags", "tags");
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
} }
createZabbixParams(args?: A): ZabbixParams { createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return { return this.optimizeZabbixParams({
...super.createZabbixParams(args), ...super.createZabbixParams(args),
selectParentTemplates: [ selectParentTemplates: [
"templateid", "templateid",
@ -43,7 +47,7 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
"description", "description",
"parentTemplates" "parentTemplates"
] ]
}; }, output);
} }
} }
@ -67,10 +71,12 @@ export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest<
export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequest<T, A> { export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequest<T, A> {
constructor(path: string, authToken?: string | null, cookie?: string) { constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie); super(path, authToken, cookie);
this.skippableZabbixParams.set("selectItems", "items");
this.impliedFields.set("state", ["items"]);
} }
createZabbixParams(args?: A): ZabbixParams { createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return { return this.optimizeZabbixParams({
...super.createZabbixParams(args), ...super.createZabbixParams(args),
selectItems: [ selectItems: [
"itemid", "itemid",
@ -99,13 +105,13 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
"description", "description",
"parentTemplates" "parentTemplates"
], ],
}; }, output);
} }
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T> { async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
let result = await super.executeRequestReturnError(zabbixAPI, args); let result = await super.executeRequestReturnError(zabbixAPI, args, output);
if (result && !isZabbixErrorResult(result)) { if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.preprocessing"))) {
const hosts = <ZabbixHost[]>result; const hosts = <ZabbixHost[]>result;
const hostids = hosts.map(h => h.hostid); const hostids = hosts.map(h => h.hostid);
@ -125,21 +131,29 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
for (let device of hosts) { for (let device of hosts) {
for (let item of device.items || []) { for (let item of device.items || []) {
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString()); item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
if (!item.lastclock ) { }
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError( }
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type)) }
if (isZabbixErrorResult(values)) { }
return values; }
}
if (values.length) { if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.lastclock") || output.includes("items.lastvalue"))) {
let latestValue = values[0]; const hosts = <ZabbixHost[]>result;
item.lastvalue = latestValue.value; for (let device of hosts) {
item.lastclock = latestValue.clock; for (let item of device.items || []) {
} else { if (!item.lastclock) {
item.lastvalue = null; let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
item.lastclock = null; zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
} if (isZabbixErrorResult(values)) {
} return values;
}
if (values.length) {
let latestValue = values[0];
item.lastvalue = latestValue.value;
item.lastclock = latestValue.clock;
} else {
item.lastvalue = null;
item.lastclock = null;
} }
} }
} }
@ -153,15 +167,16 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequestWithItems<T, A> { export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequestWithItems<T, A> {
constructor(path: string, authToken?: string | null, cookie?: string) { constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie); super(path, authToken, cookie);
this.skippableZabbixParams.set("selectInventory", "inventory");
} }
createZabbixParams(args?: A): ZabbixParams { createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return { return this.optimizeZabbixParams({
...super.createZabbixParams(args), ...super.createZabbixParams(args),
selectInventory: [ selectInventory: [
"location", "location_lat", "location_lon" "location", "location_lat", "location_lon"
] ]
}; }, output);
} }
} }
@ -201,6 +216,7 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
templateids?: [number]; templateids?: [number];
hostgroupids?: [number]; hostgroupids?: [number];
macros?: { macro: string, value: string }[]; macros?: { macro: string, value: string }[];
tags?: { tag: string, value: string }[];
additionalParams?: any; additionalParams?: any;
} }
@ -231,6 +247,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
if (inputParams.macros) { if (inputParams.macros) {
this.macros = inputParams.macros; this.macros = inputParams.macros;
} }
if (inputParams.tags) {
this.tags = inputParams.tags;
}
} }
host: string host: string
@ -246,6 +265,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
templates?: any templates?: any
groups?: any groups?: any
macros?: { macro: string, value: string }[] macros?: { macro: string, value: string }[]
tags?: { tag: string, value: string }[]
} }

View file

@ -148,17 +148,57 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
protected requestBodyTemplate: ZabbixRequestBody; protected requestBodyTemplate: ZabbixRequestBody;
protected method: string protected method: string
protected prepResult: T | ZabbixErrorResult | undefined = undefined protected prepResult: T | ZabbixErrorResult | undefined = undefined
protected skippableZabbixParams: Map<string, string> = new Map();
protected impliedFields: Map<string, string[]> = new Map();
constructor(public path: string, public authToken?: string | null, public cookie?: string | null) { constructor(public path: string, public authToken?: string | null, public cookie?: string | null) {
this.method = path.split(".", 2).join("."); this.method = path.split(".", 2).join(".");
this.requestBodyTemplate = new ZabbixRequestBody(this.method); this.requestBodyTemplate = new ZabbixRequestBody(this.method);
} }
createZabbixParams(args?: A): ZabbixParams { optimizeZabbixParams(params: ZabbixParams, output?: string[]): ZabbixParams {
return args?.zabbix_params || {} if (!output || output.length === 0) {
return params;
}
const requestedTopLevelFields = Array.from(new Set(output.map(field => field.split('.')[0])));
// Apply implied fields (e.g. "state" implies "items")
let neededTopLevelFields = [...requestedTopLevelFields];
this.impliedFields.forEach((implied, field) => {
if (requestedTopLevelFields.includes(field)) {
neededTopLevelFields.push(...implied.map(f => f.split('.')[0]));
}
});
const topLevelOutput = Array.from(new Set(neededTopLevelFields));
// Reduce output subfields
if (params.output) {
if (Array.isArray(params.output)) {
params.output = params.output.filter(field => topLevelOutput.includes(field));
} else if (params.output === "extend") {
params.output = topLevelOutput;
}
} else {
params.output = topLevelOutput;
}
// Remove skippable parameters
this.skippableZabbixParams.forEach((neededField, skippableParam) => {
if (!topLevelOutput.includes(neededField) && params.hasOwnProperty(skippableParam)) {
delete params[skippableParam];
}
});
return params;
} }
getRequestBody(args?: A, zabbixParams?: ZabbixParams): ZabbixRequestBody { createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams(args?.zabbix_params || {}, output)
}
getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[]): ZabbixRequestBody {
let params: ZabbixParams let params: ZabbixParams
if (Array.isArray(args?.zabbix_params)) { if (Array.isArray(args?.zabbix_params)) {
params = args?.zabbix_params.map(paramsObj => { params = args?.zabbix_params.map(paramsObj => {
@ -168,7 +208,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
return paramsObj; return paramsObj;
}) })
} else { } else {
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)} params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args, output)}
} }
return params ? { return params ? {
...this.requestBodyTemplate, ...this.requestBodyTemplate,
@ -204,12 +244,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
return this.prepResult; return this.prepResult;
} }
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult> { async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
let prepareResult = await this.prepare(zabbixAPI, args); let prepareResult = await this.prepare(zabbixAPI, args);
if (prepareResult) { if (prepareResult) {
return prepareResult; return prepareResult;
} }
let requestBody = this.getRequestBody(args); let requestBody = this.getRequestBody(args, undefined, output);
try { try {
@ -236,8 +276,8 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
} }
} }
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A): Promise<T> { async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A, output?: string[]): Promise<T> {
let response = await this.executeRequestReturnError(zabbixApi, args); let response = await this.executeRequestReturnError(zabbixApi, args, output);
if (isZabbixErrorResult(response)) { if (isZabbixErrorResult(response)) {
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, { throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
extensions: { extensions: {

View file

@ -15,20 +15,21 @@ export interface ZabbixQueryTemplateResponse {
export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> { export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> {
constructor(authToken?: string | null, cookie?: string | null,) { constructor(authToken?: string | null, cookie?: string | null,) {
super("template.get", authToken, cookie); super("template.get", authToken, cookie);
this.skippableZabbixParams.set("selectItems", "items");
} }
createZabbixParams(args?: ParsedArgs): ZabbixParams { createZabbixParams(args?: ParsedArgs, output?: string[]): ZabbixParams {
return { return this.optimizeZabbixParams({
"selectItems": "extend", "selectItems": "extend",
"output": "extend", "output": "extend",
...args?.zabbix_params ...args?.zabbix_params
}; }, output);
} }
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> { async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
let result = await super.executeRequestReturnError(zabbixAPI, args); let result = await super.executeRequestReturnError(zabbixAPI, args, output);
if (result && !isZabbixErrorResult(result) && Array.isArray(result)) { if (result && !isZabbixErrorResult(result) && Array.isArray(result) && (!output || output.includes("items.preprocessing"))) {
const templateids = result.map(t => t.templateid); const templateids = result.map(t => t.templateid);
if (templateids.length > 0) { if (templateids.length > 0) {
// Batch fetch preprocessing for all items of these templates // Batch fetch preprocessing for all items of these templates

View file

@ -144,7 +144,8 @@ export class HostImporter {
location: device.location, location: device.location,
templateids: templateids, templateids: templateids,
hostgroupids: groupids, hostgroupids: groupids,
macros: device.macros macros: device.macros,
tags: [{ tag: "deviceType", value: device.deviceType }]
} }
)) ))

View file

@ -5,7 +5,12 @@ import {TemplateImporter} from "./template_importer.js";
import {TemplateDeleter} from "./template_deleter.js"; import {TemplateDeleter} from "./template_deleter.js";
import {logger} from "../logging/logger.js"; import {logger} from "../logging/logger.js";
import {zabbixAPI} from "../datasources/zabbix-api.js"; import {zabbixAPI} from "../datasources/zabbix-api.js";
import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js"; import {
ZabbixQueryDevices,
ZabbixQueryDevicesArgs,
ZabbixQueryHostsGenericRequest,
ZabbixQueryHostsGenericRequestWithItems
} 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 {ParsedArgs} from "../datasources/zabbix-request.js";
@ -39,6 +44,11 @@ export class RegressionTestExecutor {
const regGroupName = "Templates/Roadwork/Devices"; const regGroupName = "Templates/Roadwork/Devices";
const hostGroupName = "Roadwork/Devices"; const hostGroupName = "Roadwork/Devices";
// Assure template group exists
await TemplateImporter.importTemplateGroups([{
groupName: regGroupName
}], zabbixAuthToken, cookie);
const tempResult = await TemplateImporter.importTemplates([{ const tempResult = await TemplateImporter.importTemplates([{
host: regTemplateName, host: regTemplateName,
name: "Regression Test Template", name: "Regression Test Template",
@ -275,6 +285,190 @@ export class RegressionTestExecutor {
}); });
if (!metaOverallSuccess) success = false; if (!metaOverallSuccess) success = false;
// Regression 7: Query Optimization and Skippable Parameters
let optSuccess = false;
try {
const optRequest = new ZabbixQueryHostsGenericRequestWithItems("host.get", zabbixAuthToken, cookie);
// 1. Test optimization logic: items NOT requested
const testParams1 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "name"]);
const hasSelectItems1 = "selectItems" in testParams1;
const hasOutput1 = Array.isArray(testParams1.output) && testParams1.output.includes("hostid") && testParams1.output.includes("name");
// 2. Test skippable params: items requested, tags NOT requested
const testParams2 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "items"]);
const hasSelectItems2 = "selectItems" in testParams2;
const hasSelectTags2 = "selectTags" in testParams2;
optSuccess = !hasSelectItems1 && hasOutput1 && hasSelectItems2 && !hasSelectTags2;
// 3. Test indirect dependencies: state implies items
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
const hasSelectItems3 = "selectItems" in testParams3;
const hasOutput3 = Array.isArray(testParams3.output) && testParams3.output.includes("hostid") && testParams3.output.includes("items");
optSuccess = optSuccess && hasSelectItems3 && hasOutput3;
if (!optSuccess) {
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}`);
}
} catch (error) {
logger.error(`REG-OPT: Error during optimization test: ${error}`);
}
steps.push({
name: "REG-OPT: Query Optimization and Skippable Parameters",
success: optSuccess,
message: optSuccess
? "Optimization logic correctly filters output fields and skippable parameters"
: "Optimization logic failed to correctly filter parameters"
});
if (!optSuccess) success = false;
// Regression 8: Empty result handling with filters
let emptySuccess = false;
try {
const emptyResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter_host: "NonExistentHost_" + Math.random()
}));
emptySuccess = Array.isArray(emptyResult) && emptyResult.length === 0;
} catch (error: any) {
logger.error(`REG-EMPTY: Error during empty result test: ${error}`);
}
steps.push({
name: "REG-EMPTY: Empty result handling",
success: emptySuccess,
message: emptySuccess ? "Correctly returned empty array for non-existent host" : "Failed to return empty array for non-existent host"
});
if (!emptySuccess) success = false;
// Regression 9: Dependent Items in Templates
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
const depTempResult = await TemplateImporter.importTemplates([{
host: depTempName,
name: "Regression Dependent Template",
groupNames: [regGroupName],
items: [
{
name: "Master Item",
type: 2, // Trapper
key: "master.item",
value_type: 4, // Text
history: "1d"
},
{
name: "Dependent Item",
type: 18, // Dependent
key: "dependent.item",
value_type: 4,
master_item: { key: "master.item" },
history: "1d"
}
]
}], zabbixAuthToken, cookie);
const depSuccess = !!depTempResult?.length && !depTempResult[0].error;
steps.push({
name: "REG-DEP: Dependent Items support",
success: depSuccess,
message: depSuccess ? "Template with master and dependent items imported successfully" : `Failed: ${depTempResult?.[0]?.message}`
});
if (!depSuccess) success = false;
// Regression 10: State sub-properties retrieval (Optimization indirect dependency)
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
const stateTempResult = await TemplateImporter.importTemplates([{
host: stateTempName,
name: "Regression State Template",
groupNames: [regGroupName],
tags: [{ tag: "deviceType", value: "GenericDevice" }],
items: [{
name: "Temperature",
type: 2, // Trapper
key: "operational.temperature",
value_type: 0, // Float
history: "1d"
}]
}], zabbixAuthToken, cookie);
const stateTempSuccess = !!stateTempResult?.length && !stateTempResult[0].error;
let stateHostSuccess = false;
let stateVerifySuccess = false;
if (stateTempSuccess) {
const stateHostResult = await HostImporter.importHosts([{
deviceKey: stateHostName,
deviceType: "GenericDevice",
groupNames: [hostGroupName],
templateNames: [stateTempName]
}], zabbixAuthToken, cookie);
stateHostSuccess = !!stateHostResult?.length && !!stateHostResult[0].hostid;
if (stateHostSuccess) {
// Query using ZabbixQueryDevices which handles state -> items mapping
const devicesResult = await new ZabbixQueryDevices(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ZabbixQueryDevicesArgs({
filter_host: stateHostName
}), ["hostid", "state.operational.temperature"]);
if (Array.isArray(devicesResult) && devicesResult.length > 0) {
const device = devicesResult[0] as any;
// Check if items were fetched (indirect dependency)
const hasItems = Array.isArray(device.items) && device.items.some((i: any) => i.key_ === "operational.temperature");
stateVerifySuccess = hasItems;
if (!hasItems) {
logger.error(`REG-STATE: Items missing in device result despite requesting state. Device: ${JSON.stringify(device)}`);
}
} else {
logger.error(`REG-STATE: Device not found after import. Result: ${JSON.stringify(devicesResult)}`);
}
}
}
const stateOverallSuccess = stateTempSuccess && stateHostSuccess && stateVerifySuccess;
steps.push({
name: "REG-STATE: State sub-properties retrieval (indirect dependency)",
success: stateOverallSuccess,
message: stateOverallSuccess
? "State sub-properties correctly trigger item fetching and are available"
: `Failed: TempImport=${stateTempSuccess}, HostImport=${stateHostSuccess}, Verify=${stateVerifySuccess}`
});
if (!stateOverallSuccess) success = false;
// Regression 11: Negative Optimization - items not requested (allDevices)
let optNegSuccess = false;
try {
const optRequest = new ZabbixQueryDevices(zabbixAuthToken, cookie);
// Test optimization logic: items/state NOT requested
const testParams = optRequest.createZabbixParams(new ZabbixQueryDevicesArgs({}), ["hostid", "name"]);
const hasSelectItems = "selectItems" in testParams;
const hasOutputItems = Array.isArray(testParams.output) && testParams.output.includes("items");
optNegSuccess = !hasSelectItems && !hasOutputItems;
if (!optNegSuccess) {
logger.error(`REG-OPT-NEG: Negative optimization verification failed. hasSelectItems: ${hasSelectItems}, hasOutputItems: ${hasOutputItems}`);
}
} catch (error) {
logger.error(`REG-OPT-NEG: Error during negative optimization test: ${error}`);
}
steps.push({
name: "REG-OPT-NEG: Negative Optimization - items not requested (allDevices)",
success: optNegSuccess,
message: optNegSuccess
? "Optimization correctly omits items when neither items nor state are requested"
: "Optimization failed to omit items when not requested"
});
if (!optNegSuccess) 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
@ -296,6 +490,9 @@ export class RegressionTestExecutor {
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);
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie); await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, depTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, stateTempName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, stateHostName, zabbixAuthToken, cookie);
// We don't delete the group here as it might be shared or used by other tests in this run // We don't delete the group here as it might be shared or used by other tests in this run
} catch (error: any) { } catch (error: any) {

View file

@ -0,0 +1,113 @@
import {createResolvers} from "../api/resolvers.js";
import {zabbixAPI} from "../datasources/zabbix-api.js";
import {QueryAllDevicesArgs} from "../schema/generated/graphql.js";
// Mocking ZabbixAPI
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
baseURL: "http://mock-zabbix",
}
}));
// Mocking Config
jest.mock("../common_utils.js", () => ({
Config: {
HOST_TYPE_FILTER_DEFAULT: null,
HOST_GROUP_FILTER_DEFAULT: null
}
}));
describe("Indirect Dependencies Optimization", () => {
let resolvers: any;
beforeEach(() => {
jest.clearAllMocks();
resolvers = createResolvers();
});
test("allDevices optimization - state implies items", async () => {
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
const args: QueryAllDevicesArgs = {};
const context = {
zabbixAuthToken: "test-token",
dataSources: { zabbixAPI: zabbixAPI }
};
const info = {
fieldNodes: [{
selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'hostid' } },
{ kind: 'Field', name: { value: 'state' } }
]
}
}]
};
await resolvers.Query.allDevices(null, args, context, info);
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.output).toContain("items");
expect(callParams.selectItems).toBeDefined();
});
test("allHosts optimization - inventory implies selectInventory", async () => {
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
const args = {};
const context = {
zabbixAuthToken: "test-token",
dataSources: { zabbixAPI: zabbixAPI }
};
const info = {
fieldNodes: [{
selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'inventory' } }
]
}
}]
};
await resolvers.Query.allHosts(null, args, context, info);
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
// Zabbix inventory data is requested via selectInventory, and it maps to GraphQL 'inventory' field
expect(callParams.selectInventory).toBeDefined();
});
test("allHosts optimization - state fragment implies items", async () => {
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
const args = {};
const context = {
zabbixAuthToken: "test-token",
dataSources: { zabbixAPI: zabbixAPI }
};
const info = {
fieldNodes: [{
selectionSet: {
selections: [
{
kind: 'InlineFragment',
typeCondition: { kind: 'NamedType', name: { value: 'Device' } },
selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'state' } }
]
}
}
]
}
}]
};
await resolvers.Query.allHosts(null, args, context, info);
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.output).toContain("items");
expect(callParams.selectItems).toBeDefined();
});
});

View file

@ -0,0 +1,191 @@
import {createResolvers} from "../api/resolvers.js";
import {zabbixAPI} from "../datasources/zabbix-api.js";
import {QueryAllHostsArgs, QueryTemplatesArgs} from "../schema/generated/graphql.js";
// Mocking ZabbixAPI
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
baseURL: "http://mock-zabbix",
}
}));
// Mocking Config
jest.mock("../common_utils.js", () => ({
Config: {
HOST_TYPE_FILTER_DEFAULT: null,
HOST_GROUP_FILTER_DEFAULT: null
}
}));
describe("Query Optimization", () => {
let resolvers: any;
beforeEach(() => {
jest.clearAllMocks();
resolvers = createResolvers();
});
test("allHosts optimization - reduce output and skip selectTags/selectItems", 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: 'name' } }
]
}
}]
};
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", "name"]
})
})
}));
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.selectTags).toBeUndefined();
expect(callParams.selectItems).toBeUndefined();
expect(callParams.selectParentTemplates).toBeUndefined();
// Verify no follow-up item.get call
const itemGetCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "item.get");
expect(itemGetCall).toBeUndefined();
});
test("allHosts optimization - keep selectTags when 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: 'tags' } }
]
}
}]
};
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("templates optimization - reduce output and skip selectItems", async () => {
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
const args: QueryTemplatesArgs = {};
const context = {
zabbixAuthToken: "test-token",
dataSources: { zabbixAPI: zabbixAPI }
};
const info = {
fieldNodes: [{
selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'name' } }
]
}
}]
};
await resolvers.Query.templates(null, args, context, info);
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
body: expect.objectContaining({
params: expect.objectContaining({
output: ["name"]
})
})
}));
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.selectItems).toBeUndefined();
});
test("allHosts optimization - indirect dependency via nested state field", 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: 'state' }, selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'operational' }, selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'temperature' } }
]
} }
]
} }
]
}
}]
};
await resolvers.Query.allHosts(null, args, context, info);
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.selectItems).toBeDefined();
expect(Array.isArray(callParams.output)).toBe(true);
expect(callParams.output).toContain("items");
});
test("allDevices optimization - skip items when not requested", async () => {
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
const args: any = {};
const context = {
zabbixAuthToken: "test-token",
dataSources: { zabbixAPI: zabbixAPI }
};
const info = {
fieldNodes: [{
selectionSet: {
selections: [
{ kind: 'Field', name: { value: 'hostid' } },
{ kind: 'Field', name: { value: 'name' } }
]
}
}]
};
await resolvers.Query.allDevices(null, args, context, info);
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
expect(callParams.selectItems).toBeUndefined();
expect(callParams.output).not.toContain("items");
});
});