feat(api): Enhance session events API with pagination, sorting, and filtering

- Added support for pagination (page and limit) in the session events endpoint.
- Implemented sorting functionality based on specified columns and directions.
- Introduced free-text search capability for session events.
- Updated SQL queries to retrieve all events and added a new SQL constant for events.
- Refactored GraphQL types and helpers to support new plugin and event queries.
- Created new GraphQL resolvers for plugins and events with pagination and filtering.
- Added comprehensive tests for new GraphQL endpoints and session events functionality.
This commit is contained in:
Jokob @NetAlertX
2026-03-26 20:57:10 +00:00
parent 250e533655
commit ec3e4c8988
15 changed files with 1312 additions and 352 deletions

View File

@@ -1,7 +1,5 @@
import graphene
from graphene import (
ObjectType, String, Int, Boolean, List, Field, InputObjectType, Argument
)
from graphene import ObjectType, List, Field, Argument, String
import json
import sys
import os
@@ -19,175 +17,30 @@ from helper import ( # noqa: E402 [flake8 lint suppression]
get_setting_value,
)
# Define a base URL with the user's home directory
from .graphql_types import ( # noqa: E402 [flake8 lint suppression]
FilterOptionsInput, PageQueryOptionsInput,
Device, DeviceResult,
Setting, SettingResult,
LangString, LangStringResult,
AppEvent, AppEventResult,
PluginQueryOptionsInput, PluginEntry,
PluginsObjectsResult, PluginsEventsResult, PluginsHistoryResult,
EventQueryOptionsInput, EventEntry, EventsResult,
)
from .graphql_helpers import ( # noqa: E402 [flake8 lint suppression]
mixed_type_sort_key,
apply_common_pagination,
apply_plugin_filters,
apply_events_filters,
)
folder = apiPath
# --- DEVICES ---
# Pagination and Sorting Input Types
class SortOptionsInput(InputObjectType):
field = String()
order = String()
class FilterOptionsInput(InputObjectType):
filterColumn = String()
filterValue = String()
class PageQueryOptionsInput(InputObjectType):
page = Int()
limit = Int()
sort = List(SortOptionsInput)
search = String()
status = String()
filters = List(FilterOptionsInput)
# Device ObjectType
class Device(ObjectType):
rowid = Int(description="Database row ID")
devMac = String(description="Device MAC address (e.g., 00:11:22:33:44:55)")
devName = String(description="Device display name/alias")
devOwner = String(description="Device owner")
devType = String(description="Device type classification")
devVendor = String(description="Hardware vendor from OUI lookup")
devFavorite = Int(description="Favorite flag (0 or 1)")
devGroup = String(description="Device group")
devComments = String(description="User comments")
devFirstConnection = String(description="Timestamp of first discovery")
devLastConnection = String(description="Timestamp of last connection")
devLastIP = String(description="Last known IP address")
devPrimaryIPv4 = String(description="Primary IPv4 address")
devPrimaryIPv6 = String(description="Primary IPv6 address")
devVlan = String(description="VLAN identifier")
devForceStatus = String(description="Force device status (online/offline/dont_force)")
devStaticIP = Int(description="Static IP flag (0 or 1)")
devScan = Int(description="Scan flag (0 or 1)")
devLogEvents = Int(description="Log events flag (0 or 1)")
devAlertEvents = Int(description="Alert events flag (0 or 1)")
devAlertDown = Int(description="Alert on down flag (0 or 1)")
devSkipRepeated = Int(description="Skip repeated alerts flag (0 or 1)")
devLastNotification = String(description="Timestamp of last notification")
devPresentLastScan = Int(description="Present in last scan flag (0 or 1)")
devIsNew = Int(description="Is new device flag (0 or 1)")
devLocation = String(description="Device location")
devIsArchived = Int(description="Is archived flag (0 or 1)")
devParentMAC = String(description="Parent device MAC address")
devParentPort = String(description="Parent device port")
devIcon = String(description="Base64-encoded HTML/SVG markup used to render the device icon")
devGUID = String(description="Unique device GUID")
devSite = String(description="Site name")
devSSID = String(description="SSID connected to")
devSyncHubNode = String(description="Sync hub node name")
devSourcePlugin = String(description="Plugin that discovered the device")
devCustomProps = String(description="Base64-encoded custom properties in JSON format")
devStatus = String(description="Online/Offline status")
devIsRandomMac = Int(description="Calculated: Is MAC address randomized?")
devParentChildrenCount = Int(description="Calculated: Number of children attached to this parent")
devIpLong = String(description="Calculated: IP address in long format (returned as string to support the full unsigned 32-bit range)")
devFilterStatus = String(description="Calculated: Device status for UI filtering")
devFQDN = String(description="Fully Qualified Domain Name")
devParentRelType = String(description="Relationship type to parent")
devReqNicsOnline = Int(description="Required NICs online flag")
devMacSource = String(description="Source tracking for devMac (USER, LOCKED, NEWDEV, or plugin prefix)")
devNameSource = String(description="Source tracking for devName (USER, LOCKED, NEWDEV, or plugin prefix)")
devFQDNSource = String(description="Source tracking for devFQDN (USER, LOCKED, NEWDEV, or plugin prefix)")
devLastIPSource = String(description="Source tracking for devLastIP (USER, LOCKED, NEWDEV, or plugin prefix)")
devVendorSource = String(description="Source tracking for devVendor (USER, LOCKED, NEWDEV, or plugin prefix)")
devSSIDSource = String(description="Source tracking for devSSID (USER, LOCKED, NEWDEV, or plugin prefix)")
devParentMACSource = String(description="Source tracking for devParentMAC (USER, LOCKED, NEWDEV, or plugin prefix)")
devParentPortSource = String(description="Source tracking for devParentPort (USER, LOCKED, NEWDEV, or plugin prefix)")
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
devVlanSource = String(description="Source tracking for devVlan")
devFlapping = Int(description="Indicates flapping device (device changing between online/offline states frequently)")
devCanSleep = Int(description="Can this device sleep? (0 or 1). When enabled, offline periods within NTFPRCS_sleep_time are reported as Sleeping instead of Down.")
devIsSleeping = Int(description="Computed: Is device currently in a sleep window? (0 or 1)")
class DeviceResult(ObjectType):
devices = List(Device)
count = Int()
db_count = Int(description="Total device count in the database, before any status/filter/search is applied")
# --- SETTINGS ---
# Setting ObjectType
class Setting(ObjectType):
setKey = String(description="Unique configuration key")
setName = String(description="Human-readable setting name")
setDescription = String(description="Detailed description of the setting")
setType = String(description="Config-driven type definition used to determine value type and UI rendering")
setOptions = String(description="JSON string of available options")
setGroup = String(description="UI group for categorization")
setValue = String(description="Current value")
setEvents = String(description="JSON string of events")
setOverriddenByEnv = Boolean(description="Whether the value is currently overridden by an environment variable")
class SettingResult(ObjectType):
settings = List(Setting, description="List of setting objects")
count = Int(description="Total count of settings")
# --- LANGSTRINGS ---
# In-memory cache for lang strings
_langstrings_cache = {} # caches lists per file (core JSON or plugin)
_langstrings_cache_mtime = {} # tracks last modified times
# LangString ObjectType
class LangString(ObjectType):
langCode = String(description="Language code (e.g., en_us, de_de)")
langStringKey = String(description="Unique translation key")
langStringText = String(description="Translated text content")
class LangStringResult(ObjectType):
langStrings = List(LangString, description="List of language string objects")
count = Int(description="Total count of strings")
# --- APP EVENTS ---
class AppEvent(ObjectType):
index = Int(description="Internal index")
guid = String(description="Unique event GUID")
appEventProcessed = Int(description="Processing status (0 or 1)")
dateTimeCreated = String(description="Event creation timestamp")
objectType = String(description="Type of the related object (Device, Setting, etc.)")
objectGuid = String(description="GUID of the related object")
objectPlugin = String(description="Plugin associated with the object")
objectPrimaryId = String(description="Primary identifier of the object")
objectSecondaryId = String(description="Secondary identifier of the object")
objectForeignKey = String(description="Foreign key reference")
objectIndex = Int(description="Object index")
objectIsNew = Int(description="Is the object new? (0 or 1)")
objectIsArchived = Int(description="Is the object archived? (0 or 1)")
objectStatusColumn = String(description="Column used for status")
objectStatus = String(description="Object status value")
appEventType = String(description="Type of application event")
helper1 = String(description="Generic helper field 1")
helper2 = String(description="Generic helper field 2")
helper3 = String(description="Generic helper field 3")
extra = String(description="Additional JSON data")
class AppEventResult(ObjectType):
appEvents = List(AppEvent, description="List of application events")
count = Int(description="Total count of events")
# ----------------------------------------------------------------------------------------------
# Define Query Type with Pagination Support
class Query(ObjectType):
# --- DEVICES ---
devices = Field(DeviceResult, options=PageQueryOptionsInput())
@@ -652,15 +505,75 @@ class Query(ObjectType):
return LangStringResult(langStrings=langStrings, count=len(langStrings))
# --- PLUGINS_OBJECTS ---
pluginsObjects = Field(PluginsObjectsResult, options=PluginQueryOptionsInput())
# helps sorting inconsistent dataset mixed integers and strings
def mixed_type_sort_key(value):
if value is None or value == "":
return (2, "") # Place None or empty strings last
def resolve_pluginsObjects(self, info, options=None):
return _resolve_plugin_table("table_plugins_objects.json", options, PluginsObjectsResult)
# --- PLUGINS_EVENTS ---
pluginsEvents = Field(PluginsEventsResult, options=PluginQueryOptionsInput())
def resolve_pluginsEvents(self, info, options=None):
return _resolve_plugin_table("table_plugins_events.json", options, PluginsEventsResult)
# --- PLUGINS_HISTORY ---
pluginsHistory = Field(PluginsHistoryResult, options=PluginQueryOptionsInput())
def resolve_pluginsHistory(self, info, options=None):
return _resolve_plugin_table("table_plugins_history.json", options, PluginsHistoryResult)
# --- EVENTS ---
events = Field(EventsResult, options=EventQueryOptionsInput())
def resolve_events(self, info, options=None):
try:
with open(folder + "table_events.json", "r") as f:
data = json.load(f).get("data", [])
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog("none", f"[graphql_schema] Error loading events data: {e}")
return EventsResult(entries=[], count=0, db_count=0)
db_count = len(data)
data = apply_events_filters(data, options)
data, total_count = apply_common_pagination(data, options)
return EventsResult(
entries=[EventEntry(**r) for r in data],
count=total_count,
db_count=db_count,
)
# ---------------------------------------------------------------------------
# Private resolver helper — shared by all three plugin table resolvers
# ---------------------------------------------------------------------------
def _resolve_plugin_table(json_file, options, ResultType):
try:
return (0, int(value)) # Integers get priority
except (ValueError, TypeError):
return (1, str(value)) # Strings come next
with open(folder + json_file, "r") as f:
data = json.load(f).get("data", [])
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog("none", f"[graphql_schema] Error loading {json_file}: {e}")
return ResultType(entries=[], count=0, db_count=0)
# Scope to the requested plugin + foreignKey FIRST so db_count
# reflects the total for THIS plugin, not the entire table.
if options:
if options.plugin:
pl = options.plugin.lower()
data = [r for r in data if str(r.get("plugin", "")).lower() == pl]
if options.foreignKey:
fk = options.foreignKey.lower()
data = [r for r in data if str(r.get("foreignKey", "")).lower() == fk]
db_count = len(data)
data = apply_plugin_filters(data, options)
data, total_count = apply_common_pagination(data, options)
return ResultType(
entries=[PluginEntry(**r) for r in data],
count=total_count,
db_count=db_count,
)
# Schema Definition