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)
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)
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-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
- **TC-CONF-01**: Schema loader uses Config variables.
- **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.
- **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).
- **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
@ -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-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-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-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) |

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.
## ✅ Achieved Milestones
- **🎯 VCR Product Integration**:
- 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**.
- **🎯 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**.
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
- **🔓 Open Source Extraction & AI Integration**:
- Extracted the core functionality of the API to publish it as an **Open Source** project.
- 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.
- **🔓 Open Source Extraction & AI Integration**: 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.
## 📅 Planned Enhancements
- **📦 CI/CD & Package Publishing**:
- Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
- **🧱 Flexible Usage by publishing to [npmjs.com](https://www.npmjs.com/)**:
- 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.
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
- *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`
- 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.
- **📦 CI/CD & Package Publishing**: Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
- **🧱 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 {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js";
import {Config} from "../common_utils.js";
import {GraphqlParamsToNeededZabbixOutput} from "../datasources/graphql-params-to-zabbix-output.js";
export function createResolvers(): Resolvers {
@ -102,36 +103,39 @@ export function createResolvers(): Resolvers {
allHosts: async (_parent: any, args: QueryAllHostsArgs, {
zabbixAuthToken,
cookie, dataSources
}: any) => {
}: any, info: any) => {
if (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)
.executeRequestThrowError(
dataSources.zabbixAPI, new ParsedArgs(args)
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
)
},
allDevices: async (_parent: any, args: QueryAllDevicesArgs, {
zabbixAuthToken,
cookie, dataSources
}: any) => {
}: any, info: any) => {
if (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)
.executeRequestThrowError(
dataSources.zabbixAPI, new ZabbixQueryDevicesArgs(args)
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
)
},
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
zabbixAuthToken,
cookie
}: any) => {
cookie, dataSources
}: any, info: any) => {
if (!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(
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, {
zabbixAuthToken,
cookie
}: any) => {
cookie, dataSources
}: any, info: any) => {
let params: any = {}
if (args.hostids) {
params.templateids = args.hostids
@ -169,8 +173,9 @@ export function createResolvers(): Resolvers {
name: args.name_pattern
}
}
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
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, {

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);
}
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true): Promise<T | ZabbixErrorResult> {
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args);
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, 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) {
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError);
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, output);
}
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) {
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 {
return {
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams({
...super.createZabbixParams(args),
selectParentTemplates: [
"templateid",
@ -43,7 +47,7 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
"description",
"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> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
this.skippableZabbixParams.set("selectItems", "items");
this.impliedFields.set("state", ["items"]);
}
createZabbixParams(args?: A): ZabbixParams {
return {
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams({
...super.createZabbixParams(args),
selectItems: [
"itemid",
@ -99,13 +105,13 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
"description",
"parentTemplates"
],
};
}, output);
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T> {
let result = await super.executeRequestReturnError(zabbixAPI, args);
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
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 hostids = hosts.map(h => h.hostid);
@ -125,6 +131,16 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
for (let device of hosts) {
for (let item of device.items || []) {
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
}
}
}
}
}
if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.lastclock") || output.includes("items.lastvalue"))) {
const hosts = <ZabbixHost[]>result;
for (let device of hosts) {
for (let item of device.items || []) {
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))
@ -143,8 +159,6 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
}
}
}
}
}
return result;
}
@ -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> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
this.skippableZabbixParams.set("selectInventory", "inventory");
}
createZabbixParams(args?: A): ZabbixParams {
return {
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams({
...super.createZabbixParams(args),
selectInventory: [
"location", "location_lat", "location_lon"
]
};
}, output);
}
}
@ -201,6 +216,7 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
templateids?: [number];
hostgroupids?: [number];
macros?: { macro: string, value: string }[];
tags?: { tag: string, value: string }[];
additionalParams?: any;
}
@ -231,6 +247,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
if (inputParams.macros) {
this.macros = inputParams.macros;
}
if (inputParams.tags) {
this.tags = inputParams.tags;
}
}
host: string
@ -246,6 +265,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
templates?: any
groups?: any
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 method: string
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) {
this.method = path.split(".", 2).join(".");
this.requestBodyTemplate = new ZabbixRequestBody(this.method);
}
createZabbixParams(args?: A): ZabbixParams {
return args?.zabbix_params || {}
optimizeZabbixParams(params: ZabbixParams, output?: string[]): ZabbixParams {
if (!output || output.length === 0) {
return params;
}
getRequestBody(args?: A, zabbixParams?: ZabbixParams): ZabbixRequestBody {
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;
}
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams(args?.zabbix_params || {}, output)
}
getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[]): ZabbixRequestBody {
let params: ZabbixParams
if (Array.isArray(args?.zabbix_params)) {
params = args?.zabbix_params.map(paramsObj => {
@ -168,7 +208,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
return paramsObj;
})
} else {
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)}
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args, output)}
}
return params ? {
...this.requestBodyTemplate,
@ -204,12 +244,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
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);
if (prepareResult) {
return prepareResult;
}
let requestBody = this.getRequestBody(args);
let requestBody = this.getRequestBody(args, undefined, output);
try {
@ -236,8 +276,8 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
}
}
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A): Promise<T> {
let response = await this.executeRequestReturnError(zabbixApi, args);
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A, output?: string[]): Promise<T> {
let response = await this.executeRequestReturnError(zabbixApi, args, output);
if (isZabbixErrorResult(response)) {
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
extensions: {

View file

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

View file

@ -144,7 +144,8 @@ export class HostImporter {
location: device.location,
templateids: templateids,
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 {logger} from "../logging/logger.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 {ParsedArgs} from "../datasources/zabbix-request.js";
@ -39,6 +44,11 @@ export class RegressionTestExecutor {
const regGroupName = "Templates/Roadwork/Devices";
const hostGroupName = "Roadwork/Devices";
// Assure template group exists
await TemplateImporter.importTemplateGroups([{
groupName: regGroupName
}], zabbixAuthToken, cookie);
const tempResult = await TemplateImporter.importTemplates([{
host: regTemplateName,
name: "Regression Test Template",
@ -275,6 +285,190 @@ export class RegressionTestExecutor {
});
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)
const groupResult = await HostImporter.importHostGroups([{
groupName: groupName
@ -296,6 +490,9 @@ export class RegressionTestExecutor {
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, macroTemplateName, 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
} 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");
});
});