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:
parent
ad104acde2
commit
97a0f70fd6
16 changed files with 835 additions and 69 deletions
|
|
@ -7,6 +7,9 @@ This directory contains detailed guides on how to use and extend the Zabbix Grap
|
||||||
### 🍳 [Cookbook](./cookbook.md)
|
### 🍳 [Cookbook](./cookbook.md)
|
||||||
Practical, step-by-step recipes for common tasks, designed for both humans and AI-based test generation.
|
Practical, step-by-step recipes for common tasks, designed for both humans and AI-based test generation.
|
||||||
|
|
||||||
|
### ⚡ [Query Optimization](./query_optimization.md)
|
||||||
|
Learn how the API optimizes Zabbix requests by reducing output fields and skipping unnecessary parameters based on the GraphQL query.
|
||||||
|
|
||||||
### 📊 [Schema and Schema Extension](./schema.md)
|
### 📊 [Schema and Schema Extension](./schema.md)
|
||||||
Learn about the GraphQL schema structure, how Zabbix entities map to GraphQL types, and how to use the dynamic schema extension system.
|
Learn about the GraphQL schema structure, how Zabbix entities map to GraphQL types, and how to use the dynamic schema extension system.
|
||||||
|
|
||||||
|
|
|
||||||
51
docs/howtos/query_optimization.md
Normal file
51
docs/howtos/query_optimization.md
Normal 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.
|
||||||
|
|
@ -53,6 +53,11 @@ This document outlines the test cases and coverage for the Zabbix GraphQL API.
|
||||||
- **TC-AUTH-04**: Import user rights.
|
- **TC-AUTH-04**: Import user rights.
|
||||||
- **TC-AUTH-05**: Import user rights using sample mutation.
|
- **TC-AUTH-05**: Import user rights using sample mutation.
|
||||||
|
|
||||||
|
### Query Optimization
|
||||||
|
- **TC-OPT-01**: Verify that GraphQL queries only fetch requested fields from Zabbix (reduced output).
|
||||||
|
- **TC-OPT-02**: Verify that skippable Zabbix parameters (like selectItems) are omitted if not requested in GraphQL.
|
||||||
|
- **TC-OPT-03**: Verify that indirect dependencies (e.g., `state` requiring `items`) are correctly handled by the optimization logic.
|
||||||
|
|
||||||
### System and Configuration
|
### System and Configuration
|
||||||
- **TC-CONF-01**: Schema loader uses Config variables.
|
- **TC-CONF-01**: Schema loader uses Config variables.
|
||||||
- **TC-CONF-02**: Zabbix API constants derived from Config.
|
- **TC-CONF-02**: Zabbix API constants derived from Config.
|
||||||
|
|
@ -77,6 +82,11 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
||||||
- **Template technical name lookup**: Verifies that templates can be correctly identified by their technical name (`host` field) when linking them to hosts during import.
|
- **Template technical name lookup**: Verifies that templates can be correctly identified by their technical name (`host` field) when linking them to hosts during import.
|
||||||
- **HTTP Agent URL support**: Verifies that templates containing HTTP Agent items with a configured URL can be imported successfully (verifying the addition of the `url` field to `CreateTemplateItem`).
|
- **HTTP Agent URL support**: Verifies that templates containing HTTP Agent items with a configured URL can be imported successfully (verifying the addition of the `url` field to `CreateTemplateItem`).
|
||||||
- **Host retrieval and visibility**: Verifies that newly imported hosts are immediately visible and retrievable via the `allHosts` query, including correctly delivered assigned templates and assigned host groups (verifying the fix for `output` fields in the host query data source).
|
- **Host retrieval and visibility**: Verifies that newly imported hosts are immediately visible and retrievable via the `allHosts` query, including correctly delivered assigned templates and assigned host groups (verifying the fix for `output` fields in the host query data source).
|
||||||
|
- **Query Optimization**: Verifies that GraphQL requests correctly translate into optimized Zabbix parameters, reducing the amount of data fetched (verifying the query optimization feature).
|
||||||
|
- **Empty result handling**: Verifies that queries return an empty array instead of an error when no entities match the provided filters.
|
||||||
|
- **Dependent Items**: Verifies that templates with master and dependent items can be imported successfully, correctly resolving the dependency within the same import operation.
|
||||||
|
- **State sub-properties**: Verifies that requesting device state sub-properties correctly triggers the retrieval of required Zabbix items, even if `items` is not explicitly requested (verifying the indirect dependency logic).
|
||||||
|
- **Negative Optimization (allDevices)**: Verifies that items are NOT requested from Zabbix if neither `items` nor `state` (or state sub-properties) are requested within the `allDevices` query.
|
||||||
|
|
||||||
## ✅ Test Coverage Checklist
|
## ✅ Test Coverage Checklist
|
||||||
|
|
||||||
|
|
@ -116,6 +126,9 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
||||||
| TC-AUTH-03 | hasPermissions query | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
| TC-AUTH-03 | hasPermissions query | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
||||||
| TC-AUTH-04 | importUserRights mutation | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
| TC-AUTH-04 | importUserRights mutation | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
||||||
| TC-AUTH-05 | Import user rights using sample | Integration | Jest | [src/test/user_rights_integration.test.ts](../src/test/user_rights_integration.test.ts) |
|
| TC-AUTH-05 | Import user rights using sample | Integration | Jest | [src/test/user_rights_integration.test.ts](../src/test/user_rights_integration.test.ts) |
|
||||||
|
| TC-OPT-01 | Verify Query Optimization (reduced output) | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
|
||||||
|
| TC-OPT-02 | Verify skippable parameters | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
|
||||||
|
| TC-OPT-03 | Verify indirect dependencies | Unit | Jest | [src/test/indirect_dependencies.test.ts](../src/test/indirect_dependencies.test.ts) |
|
||||||
| TC-CONF-01 | schema_loader uses Config variables | Unit | Jest | [src/test/schema_config.test.ts](../src/test/schema_config.test.ts) |
|
| TC-CONF-01 | schema_loader uses Config variables | Unit | Jest | [src/test/schema_config.test.ts](../src/test/schema_config.test.ts) |
|
||||||
| TC-CONF-02 | constants are derived from Config | Unit | Jest | [src/test/zabbix_api_config.test.ts](../src/test/zabbix_api_config.test.ts) |
|
| TC-CONF-02 | constants are derived from Config | Unit | Jest | [src/test/zabbix_api_config.test.ts](../src/test/zabbix_api_config.test.ts) |
|
||||||
| TC-CONF-03 | logger levels initialized from Config | Unit | Jest | [src/test/logger_config.test.ts](../src/test/logger_config.test.ts) |
|
| TC-CONF-03 | logger levels initialized from Config | Unit | Jest | [src/test/logger_config.test.ts](../src/test/logger_config.test.ts) |
|
||||||
|
|
|
||||||
69
docs/use-cases/trade-fair-logistics-requirements.md
Normal file
69
docs/use-cases/trade-fair-logistics-requirements.md
Normal 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)
|
||||||
29
roadmap.md
29
roadmap.md
|
|
@ -3,21 +3,26 @@
|
||||||
This document outlines the achieved milestones and planned enhancements for the Zabbix GraphQL API project.
|
This document outlines the achieved milestones and planned enhancements for the Zabbix GraphQL API project.
|
||||||
|
|
||||||
## ✅ Achieved Milestones
|
## ✅ Achieved Milestones
|
||||||
- **🎯 VCR Product Integration**:
|
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
||||||
- Developed a specialized **GraphQL API** as part of the VCR Product. This enables the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
|
||||||
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
|
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
|
||||||
|
|
||||||
- **🔓 Open Source Extraction & AI Integration**:
|
- **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
|
||||||
- Extracted the core functionality of the API to publish it as an **Open Source** project.
|
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
|
||||||
- Enhanced it with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases within the VCR or other applications.
|
|
||||||
|
|
||||||
## 📅 Planned Enhancements
|
## 📅 Planned Enhancements
|
||||||
- **📦 CI/CD & Package Publishing**:
|
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
|
||||||
- Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
|
|
||||||
|
|
||||||
- **🧱 Flexible Usage by publishing to [npmjs.com](https://www.npmjs.com/)**:
|
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
|
||||||
- Transform the package into a versatile tool that can be used either **standalone** or as a **core library**, allowing other projects to include and extend it easily.
|
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
|
||||||
|
- *Simulation*:
|
||||||
|
- Create mocked "real world sensor devices" relevant for the use case.
|
||||||
|
- Create a sample device collecting relevant information from public APIs, e.g. weather conditions or traffic conditions at a given location.
|
||||||
|
- Simulate the traffic conditions on the route by using the simulated sensor devices.
|
||||||
|
- *Configuration*: Analyze a real-world transport and configure Zabbix by placing sensor devices at the right places of the route.
|
||||||
|
|
||||||
- **🧩 Extension Project**: `zabbix-graphql-api-problems`
|
- **📦 CI/CD & Package Publishing**: Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
|
||||||
- Create the first official extension, `zabbix-graphql-api-problems`
|
|
||||||
- Add support for **problem and trigger-related** queries. This will leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows.
|
- **🧱 Flexible Usage**: Transform the package into a versatile tool that can be used either **standalone** or as a **core library** (published to [npmjs.com](https://www.npmjs.com/)).
|
||||||
|
|
||||||
|
- **🧩 Extension Project**: Add support for **problem and trigger-related** queries through the `zabbix-graphql-api-problems` extension.
|
||||||
|
- *AI Integration*: Leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows.
|
||||||
|
|
|
||||||
31
src/api/graphql_utils.ts
Normal file
31
src/api/graphql_utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,7 @@ import {GraphQLInterfaceType, GraphQLList} from "graphql/type/index.js";
|
||||||
import {isDevice} from "./resolver_helpers.js";
|
import {isDevice} from "./resolver_helpers.js";
|
||||||
import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js";
|
import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js";
|
||||||
import {Config} from "../common_utils.js";
|
import {Config} from "../common_utils.js";
|
||||||
|
import {GraphqlParamsToNeededZabbixOutput} from "../datasources/graphql-params-to-zabbix-output.js";
|
||||||
|
|
||||||
|
|
||||||
export function createResolvers(): Resolvers {
|
export function createResolvers(): Resolvers {
|
||||||
|
|
@ -102,36 +103,39 @@ export function createResolvers(): Resolvers {
|
||||||
allHosts: async (_parent: any, args: QueryAllHostsArgs, {
|
allHosts: async (_parent: any, args: QueryAllHostsArgs, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie, dataSources
|
cookie, dataSources
|
||||||
}: any) => {
|
}: any, info: any) => {
|
||||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||||
}
|
}
|
||||||
|
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
||||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
|
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(
|
.executeRequestThrowError(
|
||||||
dataSources.zabbixAPI, new ParsedArgs(args)
|
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
allDevices: async (_parent: any, args: QueryAllDevicesArgs, {
|
allDevices: async (_parent: any, args: QueryAllDevicesArgs, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie, dataSources
|
cookie, dataSources
|
||||||
}: any) => {
|
}: any, info: any) => {
|
||||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||||
}
|
}
|
||||||
|
const output = GraphqlParamsToNeededZabbixOutput.mapAllDevices(args, info);
|
||||||
return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(
|
.executeRequestThrowError(
|
||||||
dataSources.zabbixAPI, new ZabbixQueryDevicesArgs(args)
|
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
|
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie
|
cookie, dataSources
|
||||||
}: any) => {
|
}: any, info: any) => {
|
||||||
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
||||||
args.search_name = Config.HOST_GROUP_FILTER_DEFAULT
|
args.search_name = Config.HOST_GROUP_FILTER_DEFAULT
|
||||||
}
|
}
|
||||||
|
const output = GraphqlParamsToNeededZabbixOutput.mapAllHostGroups(args, info);
|
||||||
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
|
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
|
||||||
zabbixAPI, new ZabbixQueryHostgroupsParams(args)
|
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -158,8 +162,8 @@ export function createResolvers(): Resolvers {
|
||||||
|
|
||||||
templates: async (_parent: any, args: QueryTemplatesArgs, {
|
templates: async (_parent: any, args: QueryTemplatesArgs, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie
|
cookie, dataSources
|
||||||
}: any) => {
|
}: any, info: any) => {
|
||||||
let params: any = {}
|
let params: any = {}
|
||||||
if (args.hostids) {
|
if (args.hostids) {
|
||||||
params.templateids = args.hostids
|
params.templateids = args.hostids
|
||||||
|
|
@ -169,8 +173,9 @@ export function createResolvers(): Resolvers {
|
||||||
name: args.name_pattern
|
name: args.name_pattern
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
|
||||||
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
|
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
||||||
},
|
},
|
||||||
|
|
||||||
allTemplateGroups: async (_parent: any, args: any, {
|
allTemplateGroups: async (_parent: any, args: any, {
|
||||||
|
|
|
||||||
26
src/datasources/graphql-params-to-zabbix-output.ts
Normal file
26
src/datasources/graphql-params-to-zabbix-output.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,12 +80,12 @@ export class ZabbixAPI
|
||||||
return super.post(path, request);
|
return super.post(path, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true): Promise<T | ZabbixErrorResult> {
|
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||||
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args);
|
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true) {
|
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true, output?: string[]) {
|
||||||
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError);
|
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) {
|
async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) {
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,14 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
||||||
|
|
||||||
constructor(path: string, authToken?: string | null, cookie?: string | null) {
|
constructor(path: string, authToken?: string | null, cookie?: string | null) {
|
||||||
super(path, authToken, cookie);
|
super(path, authToken, cookie);
|
||||||
|
this.skippableZabbixParams.set("selectParentTemplates", "parentTemplates");
|
||||||
|
this.skippableZabbixParams.set("selectTags", "tags");
|
||||||
|
this.skippableZabbixParams.set("selectInheritedTags", "tags");
|
||||||
|
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: A): ZabbixParams {
|
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||||
return {
|
return this.optimizeZabbixParams({
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
selectParentTemplates: [
|
selectParentTemplates: [
|
||||||
"templateid",
|
"templateid",
|
||||||
|
|
@ -43,7 +47,7 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
||||||
"description",
|
"description",
|
||||||
"parentTemplates"
|
"parentTemplates"
|
||||||
]
|
]
|
||||||
};
|
}, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,10 +71,12 @@ export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest<
|
||||||
export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequest<T, A> {
|
export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequest<T, A> {
|
||||||
constructor(path: string, authToken?: string | null, cookie?: string) {
|
constructor(path: string, authToken?: string | null, cookie?: string) {
|
||||||
super(path, authToken, cookie);
|
super(path, authToken, cookie);
|
||||||
|
this.skippableZabbixParams.set("selectItems", "items");
|
||||||
|
this.impliedFields.set("state", ["items"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: A): ZabbixParams {
|
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||||
return {
|
return this.optimizeZabbixParams({
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
selectItems: [
|
selectItems: [
|
||||||
"itemid",
|
"itemid",
|
||||||
|
|
@ -99,13 +105,13 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
||||||
"description",
|
"description",
|
||||||
"parentTemplates"
|
"parentTemplates"
|
||||||
],
|
],
|
||||||
};
|
}, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T> {
|
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
|
||||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
let result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||||
|
|
||||||
if (result && !isZabbixErrorResult(result)) {
|
if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.preprocessing"))) {
|
||||||
const hosts = <ZabbixHost[]>result;
|
const hosts = <ZabbixHost[]>result;
|
||||||
const hostids = hosts.map(h => h.hostid);
|
const hostids = hosts.map(h => h.hostid);
|
||||||
|
|
||||||
|
|
@ -125,21 +131,29 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
||||||
for (let device of hosts) {
|
for (let device of hosts) {
|
||||||
for (let item of device.items || []) {
|
for (let item of device.items || []) {
|
||||||
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
|
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
|
||||||
if (!item.lastclock ) {
|
}
|
||||||
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
}
|
||||||
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
}
|
||||||
if (isZabbixErrorResult(values)) {
|
}
|
||||||
return values;
|
}
|
||||||
}
|
|
||||||
if (values.length) {
|
if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.lastclock") || output.includes("items.lastvalue"))) {
|
||||||
let latestValue = values[0];
|
const hosts = <ZabbixHost[]>result;
|
||||||
item.lastvalue = latestValue.value;
|
for (let device of hosts) {
|
||||||
item.lastclock = latestValue.clock;
|
for (let item of device.items || []) {
|
||||||
} else {
|
if (!item.lastclock) {
|
||||||
item.lastvalue = null;
|
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
||||||
item.lastclock = null;
|
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
||||||
}
|
if (isZabbixErrorResult(values)) {
|
||||||
}
|
return values;
|
||||||
|
}
|
||||||
|
if (values.length) {
|
||||||
|
let latestValue = values[0];
|
||||||
|
item.lastvalue = latestValue.value;
|
||||||
|
item.lastclock = latestValue.clock;
|
||||||
|
} else {
|
||||||
|
item.lastvalue = null;
|
||||||
|
item.lastclock = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,15 +167,16 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
||||||
export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequestWithItems<T, A> {
|
export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequestWithItems<T, A> {
|
||||||
constructor(path: string, authToken?: string | null, cookie?: string) {
|
constructor(path: string, authToken?: string | null, cookie?: string) {
|
||||||
super(path, authToken, cookie);
|
super(path, authToken, cookie);
|
||||||
|
this.skippableZabbixParams.set("selectInventory", "inventory");
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: A): ZabbixParams {
|
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||||
return {
|
return this.optimizeZabbixParams({
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
selectInventory: [
|
selectInventory: [
|
||||||
"location", "location_lat", "location_lon"
|
"location", "location_lat", "location_lon"
|
||||||
]
|
]
|
||||||
};
|
}, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,6 +216,7 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
|
||||||
templateids?: [number];
|
templateids?: [number];
|
||||||
hostgroupids?: [number];
|
hostgroupids?: [number];
|
||||||
macros?: { macro: string, value: string }[];
|
macros?: { macro: string, value: string }[];
|
||||||
|
tags?: { tag: string, value: string }[];
|
||||||
additionalParams?: any;
|
additionalParams?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,6 +247,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
||||||
if (inputParams.macros) {
|
if (inputParams.macros) {
|
||||||
this.macros = inputParams.macros;
|
this.macros = inputParams.macros;
|
||||||
}
|
}
|
||||||
|
if (inputParams.tags) {
|
||||||
|
this.tags = inputParams.tags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
host: string
|
host: string
|
||||||
|
|
@ -246,6 +265,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
||||||
templates?: any
|
templates?: any
|
||||||
groups?: any
|
groups?: any
|
||||||
macros?: { macro: string, value: string }[]
|
macros?: { macro: string, value: string }[]
|
||||||
|
tags?: { tag: string, value: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,17 +148,57 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
protected requestBodyTemplate: ZabbixRequestBody;
|
protected requestBodyTemplate: ZabbixRequestBody;
|
||||||
protected method: string
|
protected method: string
|
||||||
protected prepResult: T | ZabbixErrorResult | undefined = undefined
|
protected prepResult: T | ZabbixErrorResult | undefined = undefined
|
||||||
|
protected skippableZabbixParams: Map<string, string> = new Map();
|
||||||
|
protected impliedFields: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
constructor(public path: string, public authToken?: string | null, public cookie?: string | null) {
|
constructor(public path: string, public authToken?: string | null, public cookie?: string | null) {
|
||||||
this.method = path.split(".", 2).join(".");
|
this.method = path.split(".", 2).join(".");
|
||||||
this.requestBodyTemplate = new ZabbixRequestBody(this.method);
|
this.requestBodyTemplate = new ZabbixRequestBody(this.method);
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: A): ZabbixParams {
|
optimizeZabbixParams(params: ZabbixParams, output?: string[]): ZabbixParams {
|
||||||
return args?.zabbix_params || {}
|
if (!output || output.length === 0) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedTopLevelFields = Array.from(new Set(output.map(field => field.split('.')[0])));
|
||||||
|
|
||||||
|
// Apply implied fields (e.g. "state" implies "items")
|
||||||
|
let neededTopLevelFields = [...requestedTopLevelFields];
|
||||||
|
this.impliedFields.forEach((implied, field) => {
|
||||||
|
if (requestedTopLevelFields.includes(field)) {
|
||||||
|
neededTopLevelFields.push(...implied.map(f => f.split('.')[0]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const topLevelOutput = Array.from(new Set(neededTopLevelFields));
|
||||||
|
|
||||||
|
// Reduce output subfields
|
||||||
|
if (params.output) {
|
||||||
|
if (Array.isArray(params.output)) {
|
||||||
|
params.output = params.output.filter(field => topLevelOutput.includes(field));
|
||||||
|
} else if (params.output === "extend") {
|
||||||
|
params.output = topLevelOutput;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.output = topLevelOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove skippable parameters
|
||||||
|
this.skippableZabbixParams.forEach((neededField, skippableParam) => {
|
||||||
|
if (!topLevelOutput.includes(neededField) && params.hasOwnProperty(skippableParam)) {
|
||||||
|
delete params[skippableParam];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestBody(args?: A, zabbixParams?: ZabbixParams): ZabbixRequestBody {
|
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||||
|
return this.optimizeZabbixParams(args?.zabbix_params || {}, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[]): ZabbixRequestBody {
|
||||||
let params: ZabbixParams
|
let params: ZabbixParams
|
||||||
if (Array.isArray(args?.zabbix_params)) {
|
if (Array.isArray(args?.zabbix_params)) {
|
||||||
params = args?.zabbix_params.map(paramsObj => {
|
params = args?.zabbix_params.map(paramsObj => {
|
||||||
|
|
@ -168,7 +208,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
return paramsObj;
|
return paramsObj;
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)}
|
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args, output)}
|
||||||
}
|
}
|
||||||
return params ? {
|
return params ? {
|
||||||
...this.requestBodyTemplate,
|
...this.requestBodyTemplate,
|
||||||
|
|
@ -204,12 +244,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
return this.prepResult;
|
return this.prepResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult> {
|
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||||
let prepareResult = await this.prepare(zabbixAPI, args);
|
let prepareResult = await this.prepare(zabbixAPI, args);
|
||||||
if (prepareResult) {
|
if (prepareResult) {
|
||||||
return prepareResult;
|
return prepareResult;
|
||||||
}
|
}
|
||||||
let requestBody = this.getRequestBody(args);
|
let requestBody = this.getRequestBody(args, undefined, output);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
@ -236,8 +276,8 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A): Promise<T> {
|
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A, output?: string[]): Promise<T> {
|
||||||
let response = await this.executeRequestReturnError(zabbixApi, args);
|
let response = await this.executeRequestReturnError(zabbixApi, args, output);
|
||||||
if (isZabbixErrorResult(response)) {
|
if (isZabbixErrorResult(response)) {
|
||||||
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
|
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
|
||||||
extensions: {
|
extensions: {
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,21 @@ export interface ZabbixQueryTemplateResponse {
|
||||||
export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> {
|
export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> {
|
||||||
constructor(authToken?: string | null, cookie?: string | null,) {
|
constructor(authToken?: string | null, cookie?: string | null,) {
|
||||||
super("template.get", authToken, cookie);
|
super("template.get", authToken, cookie);
|
||||||
|
this.skippableZabbixParams.set("selectItems", "items");
|
||||||
}
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: ParsedArgs): ZabbixParams {
|
createZabbixParams(args?: ParsedArgs, output?: string[]): ZabbixParams {
|
||||||
return {
|
return this.optimizeZabbixParams({
|
||||||
"selectItems": "extend",
|
"selectItems": "extend",
|
||||||
"output": "extend",
|
"output": "extend",
|
||||||
...args?.zabbix_params
|
...args?.zabbix_params
|
||||||
};
|
}, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
|
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
|
||||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
let result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||||
|
|
||||||
if (result && !isZabbixErrorResult(result) && Array.isArray(result)) {
|
if (result && !isZabbixErrorResult(result) && Array.isArray(result) && (!output || output.includes("items.preprocessing"))) {
|
||||||
const templateids = result.map(t => t.templateid);
|
const templateids = result.map(t => t.templateid);
|
||||||
if (templateids.length > 0) {
|
if (templateids.length > 0) {
|
||||||
// Batch fetch preprocessing for all items of these templates
|
// Batch fetch preprocessing for all items of these templates
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,8 @@ export class HostImporter {
|
||||||
location: device.location,
|
location: device.location,
|
||||||
templateids: templateids,
|
templateids: templateids,
|
||||||
hostgroupids: groupids,
|
hostgroupids: groupids,
|
||||||
macros: device.macros
|
macros: device.macros,
|
||||||
|
tags: [{ tag: "deviceType", value: device.deviceType }]
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import {TemplateImporter} from "./template_importer.js";
|
||||||
import {TemplateDeleter} from "./template_deleter.js";
|
import {TemplateDeleter} from "./template_deleter.js";
|
||||||
import {logger} from "../logging/logger.js";
|
import {logger} from "../logging/logger.js";
|
||||||
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||||
import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js";
|
import {
|
||||||
|
ZabbixQueryDevices,
|
||||||
|
ZabbixQueryDevicesArgs,
|
||||||
|
ZabbixQueryHostsGenericRequest,
|
||||||
|
ZabbixQueryHostsGenericRequestWithItems
|
||||||
|
} from "../datasources/zabbix-hosts.js";
|
||||||
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
||||||
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
||||||
|
|
||||||
|
|
@ -39,6 +44,11 @@ export class RegressionTestExecutor {
|
||||||
const regGroupName = "Templates/Roadwork/Devices";
|
const regGroupName = "Templates/Roadwork/Devices";
|
||||||
const hostGroupName = "Roadwork/Devices";
|
const hostGroupName = "Roadwork/Devices";
|
||||||
|
|
||||||
|
// Assure template group exists
|
||||||
|
await TemplateImporter.importTemplateGroups([{
|
||||||
|
groupName: regGroupName
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
const tempResult = await TemplateImporter.importTemplates([{
|
const tempResult = await TemplateImporter.importTemplates([{
|
||||||
host: regTemplateName,
|
host: regTemplateName,
|
||||||
name: "Regression Test Template",
|
name: "Regression Test Template",
|
||||||
|
|
@ -274,6 +284,190 @@ export class RegressionTestExecutor {
|
||||||
: `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}`
|
: `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}`
|
||||||
});
|
});
|
||||||
if (!metaOverallSuccess) success = false;
|
if (!metaOverallSuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 7: Query Optimization and Skippable Parameters
|
||||||
|
let optSuccess = false;
|
||||||
|
try {
|
||||||
|
const optRequest = new ZabbixQueryHostsGenericRequestWithItems("host.get", zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
// 1. Test optimization logic: items NOT requested
|
||||||
|
const testParams1 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "name"]);
|
||||||
|
const hasSelectItems1 = "selectItems" in testParams1;
|
||||||
|
const hasOutput1 = Array.isArray(testParams1.output) && testParams1.output.includes("hostid") && testParams1.output.includes("name");
|
||||||
|
|
||||||
|
// 2. Test skippable params: items requested, tags NOT requested
|
||||||
|
const testParams2 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "items"]);
|
||||||
|
const hasSelectItems2 = "selectItems" in testParams2;
|
||||||
|
const hasSelectTags2 = "selectTags" in testParams2;
|
||||||
|
|
||||||
|
optSuccess = !hasSelectItems1 && hasOutput1 && hasSelectItems2 && !hasSelectTags2;
|
||||||
|
|
||||||
|
// 3. Test indirect dependencies: state implies items
|
||||||
|
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
|
||||||
|
const hasSelectItems3 = "selectItems" in testParams3;
|
||||||
|
const hasOutput3 = Array.isArray(testParams3.output) && testParams3.output.includes("hostid") && testParams3.output.includes("items");
|
||||||
|
|
||||||
|
optSuccess = optSuccess && hasSelectItems3 && hasOutput3;
|
||||||
|
|
||||||
|
if (!optSuccess) {
|
||||||
|
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
name: "REG-OPT: Query Optimization and Skippable Parameters",
|
||||||
|
success: optSuccess,
|
||||||
|
message: optSuccess
|
||||||
|
? "Optimization logic correctly filters output fields and skippable parameters"
|
||||||
|
: "Optimization logic failed to correctly filter parameters"
|
||||||
|
});
|
||||||
|
if (!optSuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 8: Empty result handling with filters
|
||||||
|
let emptySuccess = false;
|
||||||
|
try {
|
||||||
|
const emptyResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
|
||||||
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||||
|
filter_host: "NonExistentHost_" + Math.random()
|
||||||
|
}));
|
||||||
|
|
||||||
|
emptySuccess = Array.isArray(emptyResult) && emptyResult.length === 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`REG-EMPTY: Error during empty result test: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
name: "REG-EMPTY: Empty result handling",
|
||||||
|
success: emptySuccess,
|
||||||
|
message: emptySuccess ? "Correctly returned empty array for non-existent host" : "Failed to return empty array for non-existent host"
|
||||||
|
});
|
||||||
|
if (!emptySuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 9: Dependent Items in Templates
|
||||||
|
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const depTempResult = await TemplateImporter.importTemplates([{
|
||||||
|
host: depTempName,
|
||||||
|
name: "Regression Dependent Template",
|
||||||
|
groupNames: [regGroupName],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "Master Item",
|
||||||
|
type: 2, // Trapper
|
||||||
|
key: "master.item",
|
||||||
|
value_type: 4, // Text
|
||||||
|
history: "1d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dependent Item",
|
||||||
|
type: 18, // Dependent
|
||||||
|
key: "dependent.item",
|
||||||
|
value_type: 4,
|
||||||
|
master_item: { key: "master.item" },
|
||||||
|
history: "1d"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
const depSuccess = !!depTempResult?.length && !depTempResult[0].error;
|
||||||
|
steps.push({
|
||||||
|
name: "REG-DEP: Dependent Items support",
|
||||||
|
success: depSuccess,
|
||||||
|
message: depSuccess ? "Template with master and dependent items imported successfully" : `Failed: ${depTempResult?.[0]?.message}`
|
||||||
|
});
|
||||||
|
if (!depSuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 10: State sub-properties retrieval (Optimization indirect dependency)
|
||||||
|
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
const stateTempResult = await TemplateImporter.importTemplates([{
|
||||||
|
host: stateTempName,
|
||||||
|
name: "Regression State Template",
|
||||||
|
groupNames: [regGroupName],
|
||||||
|
tags: [{ tag: "deviceType", value: "GenericDevice" }],
|
||||||
|
items: [{
|
||||||
|
name: "Temperature",
|
||||||
|
type: 2, // Trapper
|
||||||
|
key: "operational.temperature",
|
||||||
|
value_type: 0, // Float
|
||||||
|
history: "1d"
|
||||||
|
}]
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
const stateTempSuccess = !!stateTempResult?.length && !stateTempResult[0].error;
|
||||||
|
let stateHostSuccess = false;
|
||||||
|
let stateVerifySuccess = false;
|
||||||
|
|
||||||
|
if (stateTempSuccess) {
|
||||||
|
const stateHostResult = await HostImporter.importHosts([{
|
||||||
|
deviceKey: stateHostName,
|
||||||
|
deviceType: "GenericDevice",
|
||||||
|
groupNames: [hostGroupName],
|
||||||
|
templateNames: [stateTempName]
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
stateHostSuccess = !!stateHostResult?.length && !!stateHostResult[0].hostid;
|
||||||
|
|
||||||
|
if (stateHostSuccess) {
|
||||||
|
// Query using ZabbixQueryDevices which handles state -> items mapping
|
||||||
|
const devicesResult = await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||||
|
.executeRequestReturnError(zabbixAPI, new ZabbixQueryDevicesArgs({
|
||||||
|
filter_host: stateHostName
|
||||||
|
}), ["hostid", "state.operational.temperature"]);
|
||||||
|
|
||||||
|
if (Array.isArray(devicesResult) && devicesResult.length > 0) {
|
||||||
|
const device = devicesResult[0] as any;
|
||||||
|
// Check if items were fetched (indirect dependency)
|
||||||
|
const hasItems = Array.isArray(device.items) && device.items.some((i: any) => i.key_ === "operational.temperature");
|
||||||
|
stateVerifySuccess = hasItems;
|
||||||
|
|
||||||
|
if (!hasItems) {
|
||||||
|
logger.error(`REG-STATE: Items missing in device result despite requesting state. Device: ${JSON.stringify(device)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(`REG-STATE: Device not found after import. Result: ${JSON.stringify(devicesResult)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateOverallSuccess = stateTempSuccess && stateHostSuccess && stateVerifySuccess;
|
||||||
|
steps.push({
|
||||||
|
name: "REG-STATE: State sub-properties retrieval (indirect dependency)",
|
||||||
|
success: stateOverallSuccess,
|
||||||
|
message: stateOverallSuccess
|
||||||
|
? "State sub-properties correctly trigger item fetching and are available"
|
||||||
|
: `Failed: TempImport=${stateTempSuccess}, HostImport=${stateHostSuccess}, Verify=${stateVerifySuccess}`
|
||||||
|
});
|
||||||
|
if (!stateOverallSuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 11: Negative Optimization - items not requested (allDevices)
|
||||||
|
let optNegSuccess = false;
|
||||||
|
try {
|
||||||
|
const optRequest = new ZabbixQueryDevices(zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
// Test optimization logic: items/state NOT requested
|
||||||
|
const testParams = optRequest.createZabbixParams(new ZabbixQueryDevicesArgs({}), ["hostid", "name"]);
|
||||||
|
const hasSelectItems = "selectItems" in testParams;
|
||||||
|
const hasOutputItems = Array.isArray(testParams.output) && testParams.output.includes("items");
|
||||||
|
|
||||||
|
optNegSuccess = !hasSelectItems && !hasOutputItems;
|
||||||
|
|
||||||
|
if (!optNegSuccess) {
|
||||||
|
logger.error(`REG-OPT-NEG: Negative optimization verification failed. hasSelectItems: ${hasSelectItems}, hasOutputItems: ${hasOutputItems}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`REG-OPT-NEG: Error during negative optimization test: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
name: "REG-OPT-NEG: Negative Optimization - items not requested (allDevices)",
|
||||||
|
success: optNegSuccess,
|
||||||
|
message: optNegSuccess
|
||||||
|
? "Optimization correctly omits items when neither items nor state are requested"
|
||||||
|
: "Optimization failed to omit items when not requested"
|
||||||
|
});
|
||||||
|
if (!optNegSuccess) success = false;
|
||||||
|
|
||||||
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
||||||
const groupResult = await HostImporter.importHostGroups([{
|
const groupResult = await HostImporter.importHostGroups([{
|
||||||
|
|
@ -296,6 +490,9 @@ export class RegressionTestExecutor {
|
||||||
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
||||||
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
||||||
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
|
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
|
||||||
|
await TemplateDeleter.deleteTemplates(null, depTempName, zabbixAuthToken, cookie);
|
||||||
|
await TemplateDeleter.deleteTemplates(null, stateTempName, zabbixAuthToken, cookie);
|
||||||
|
await HostDeleter.deleteHosts(null, stateHostName, zabbixAuthToken, cookie);
|
||||||
// We don't delete the group here as it might be shared or used by other tests in this run
|
// We don't delete the group here as it might be shared or used by other tests in this run
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
113
src/test/indirect_dependencies.test.ts
Normal file
113
src/test/indirect_dependencies.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
191
src/test/query_optimization.test.ts
Normal file
191
src/test/query_optimization.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue