diff --git a/docs/howtos/README.md b/docs/howtos/README.md index a66a9de..43ed518 100644 --- a/docs/howtos/README.md +++ b/docs/howtos/README.md @@ -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. diff --git a/docs/howtos/query_optimization.md b/docs/howtos/query_optimization.md new file mode 100644 index 0000000..95bcfff --- /dev/null +++ b/docs/howtos/query_optimization.md @@ -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. diff --git a/docs/tests.md b/docs/tests.md index cfaa677..6806fdd 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -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) | diff --git a/docs/use-cases/trade-fair-logistics-requirements.md b/docs/use-cases/trade-fair-logistics-requirements.md new file mode 100644 index 0000000..b29d3a5 --- /dev/null +++ b/docs/use-cases/trade-fair-logistics-requirements.md @@ -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) diff --git a/roadmap.md b/roadmap.md index 2f142ff..4abf77e 100644 --- a/roadmap.md +++ b/roadmap.md @@ -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. diff --git a/src/api/graphql_utils.ts b/src/api/graphql_utils.ts new file mode 100644 index 0000000..7462c9c --- /dev/null +++ b/src/api/graphql_utils.ts @@ -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; +} diff --git a/src/api/resolvers.ts b/src/api/resolvers.ts index cf68f51..0362247 100644 --- a/src/api/resolvers.ts +++ b/src/api/resolvers.ts @@ -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, { diff --git a/src/datasources/graphql-params-to-zabbix-output.ts b/src/datasources/graphql-params-to-zabbix-output.ts new file mode 100644 index 0000000..9372644 --- /dev/null +++ b/src/datasources/graphql-params-to-zabbix-output.ts @@ -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); + } +} diff --git a/src/datasources/zabbix-api.ts b/src/datasources/zabbix-api.ts index fae182c..17ed27c 100644 --- a/src/datasources/zabbix-api.ts +++ b/src/datasources/zabbix-api.ts @@ -80,12 +80,12 @@ export class ZabbixAPI return super.post(path, request); } - async executeRequest(zabbixRequest: ZabbixRequest, args?: A, throwApiError: boolean = true): Promise { - return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args); + async executeRequest(zabbixRequest: ZabbixRequest, args?: A, throwApiError: boolean = true, output?: string[]): Promise { + return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output); } - async requestByPath(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true) { - return this.executeRequest(new ZabbixRequest(path, authToken, cookies), args, throwApiError); + async requestByPath(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true, output?: string[]) { + return this.executeRequest(new ZabbixRequest(path, authToken, cookies), args, throwApiError, output); } async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) { diff --git a/src/datasources/zabbix-hosts.ts b/src/datasources/zabbix-hosts.ts index f021fa4..e476c67 100644 --- a/src/datasources/zabbix-hosts.ts +++ b/src/datasources/zabbix-hosts.ts @@ -17,10 +17,14 @@ export class ZabbixQueryHostsGenericRequest extends ZabbixQueryHostsGenericRequest { 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 { - let result = await super.executeRequestReturnError(zabbixAPI, args); + async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise { + let result = await super.executeRequestReturnError(zabbixAPI, args, output); - if (result && !isZabbixErrorResult(result)) { + if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.preprocessing"))) { const hosts = result; const hostids = hosts.map(h => h.hostid); @@ -125,21 +131,29 @@ export class ZabbixQueryHostsGenericRequestWithItemsresult; + 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)) + 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 extends ZabbixQueryHostsGenericRequestWithItems { 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 }[] } diff --git a/src/datasources/zabbix-request.ts b/src/datasources/zabbix-request.ts index a02fb13..051024e 100644 --- a/src/datasources/zabbix-request.ts +++ b/src/datasources/zabbix-request.ts @@ -148,17 +148,57 @@ export class ZabbixRequest = new Map(); + protected impliedFields: Map = 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; + } + + 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 if (Array.isArray(args?.zabbix_params)) { params = args?.zabbix_params.map(paramsObj => { @@ -168,7 +208,7 @@ export class ZabbixRequest { + async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise { 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 { - let response = await this.executeRequestReturnError(zabbixApi, args); + async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A, output?: string[]): Promise { + 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: { diff --git a/src/datasources/zabbix-templates.ts b/src/datasources/zabbix-templates.ts index 5c08443..a872249 100644 --- a/src/datasources/zabbix-templates.ts +++ b/src/datasources/zabbix-templates.ts @@ -15,20 +15,21 @@ export interface ZabbixQueryTemplateResponse { export class ZabbixQueryTemplatesRequest extends ZabbixRequest { 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 { - let result = await super.executeRequestReturnError(zabbixAPI, args); + async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise { + 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 diff --git a/src/execution/host_importer.ts b/src/execution/host_importer.ts index 8198544..f5624e6 100644 --- a/src/execution/host_importer.ts +++ b/src/execution/host_importer.ts @@ -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 }] } )) diff --git a/src/execution/regression_test_executor.ts b/src/execution/regression_test_executor.ts index eefd8c8..f1af70b 100644 --- a/src/execution/regression_test_executor.ts +++ b/src/execution/regression_test_executor.ts @@ -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", @@ -274,6 +284,190 @@ export class RegressionTestExecutor { : `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}` }); 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([{ @@ -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) { diff --git a/src/test/indirect_dependencies.test.ts b/src/test/indirect_dependencies.test.ts new file mode 100644 index 0000000..19d82cd --- /dev/null +++ b/src/test/indirect_dependencies.test.ts @@ -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(); + }); +}); diff --git a/src/test/query_optimization.test.ts b/src/test/query_optimization.test.ts new file mode 100644 index 0000000..37a4dd5 --- /dev/null +++ b/src/test/query_optimization.test.ts @@ -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"); + }); +});