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

@@ -1750,8 +1750,13 @@ def api_device_sessions(mac, payload=None):
summary="Get Session Events",
description="Retrieve events associated with sessions.",
query_params=[
{"name": "type", "description": "Event type", "required": False, "schema": {"type": "string", "default": "all"}},
{"name": "period", "description": "Time period", "required": False, "schema": {"type": "string", "default": "7 days"}}
{"name": "type", "description": "Event type", "required": False, "schema": {"type": "string", "default": "all"}},
{"name": "period", "description": "Time period", "required": False, "schema": {"type": "string", "default": "7 days"}},
{"name": "page", "description": "Page number (1-based)", "required": False, "schema": {"type": "integer", "default": 1}},
{"name": "limit", "description": "Rows per page (max 1000)", "required": False, "schema": {"type": "integer", "default": 100}},
{"name": "search", "description": "Free-text search filter", "required": False, "schema": {"type": "string"}},
{"name": "sortCol", "description": "Column index to sort by (0-based)", "required": False, "schema": {"type": "integer", "default": 0}},
{"name": "sortDir", "description": "Sort direction: asc or desc", "required": False, "schema": {"type": "string", "default": "desc"}}
],
tags=["sessions"],
auth_callable=is_authorized
@@ -1759,7 +1764,12 @@ def api_device_sessions(mac, payload=None):
def api_get_session_events(payload=None):
session_event_type = request.args.get("type", "all")
period = get_date_from_period(request.args.get("period", "7 days"))
return get_session_events(session_event_type, period)
page = request.args.get("page", 1, type=int)
limit = request.args.get("limit", 100, type=int)
search = request.args.get("search", None)
sort_col = request.args.get("sortCol", 0, type=int)
sort_dir = request.args.get("sortDir", "desc")
return get_session_events(session_event_type, period, page=page, limit=limit, search=search, sort_col=sort_col, sort_dir=sort_dir)
# --------------------------

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

View File

@@ -0,0 +1,140 @@
"""
graphql_helpers.py — Shared utility functions for GraphQL resolvers.
"""
_MAX_LIMIT = 1000
_DEFAULT_LIMIT = 100
def mixed_type_sort_key(value):
"""Sort key that handles mixed int/string datasets without crashing.
Ordering priority:
0 — integers (sorted numerically)
1 — strings (sorted lexicographically)
2 — None / empty string (always last)
"""
if value is None or value == "":
return (2, "")
try:
return (0, int(value))
except (ValueError, TypeError):
return (1, str(value))
def apply_common_pagination(data, options):
"""Apply sort + capture total_count + paginate.
Returns (paged_data, total_count).
Enforces a hard limit cap of _MAX_LIMIT — never returns unbounded results.
"""
if not options:
return data, len(data)
# --- SORT ---
if options.sort:
for sort_option in reversed(options.sort):
field = sort_option.field
reverse = (sort_option.order or "asc").lower() == "desc"
data = sorted(
data,
key=lambda x: mixed_type_sort_key(x.get(field)),
reverse=reverse,
)
total_count = len(data)
# --- PAGINATE ---
if options.page is not None and options.limit is not None:
effective_limit = min(options.limit, _MAX_LIMIT)
start = (options.page - 1) * effective_limit
end = start + effective_limit
data = data[start:end]
return data, total_count
def apply_plugin_filters(data, options):
"""Filter a list of plugin table rows (Plugins_Objects/Events/History).
Handles: date range, column filters, free-text search.
NOTE: plugin prefix and foreignKey scoping is done in the resolver
BEFORE db_count is captured — do NOT duplicate here.
"""
if not options:
return data
# Date-range filter on dateTimeCreated
if options.dateFrom:
data = [r for r in data if str(r.get("dateTimeCreated", "")) >= options.dateFrom]
if options.dateTo:
data = [r for r in data if str(r.get("dateTimeCreated", "")) <= options.dateTo]
# Column-value exact-match filters
if options.filters:
for f in options.filters:
if f.filterColumn and f.filterValue is not None:
data = [
r for r in data
if str(r.get(f.filterColumn, "")).lower() == str(f.filterValue).lower()
]
# Free-text search
if options.search:
term = options.search.lower()
searchable = [
"plugin", "objectPrimaryId", "objectSecondaryId",
"watchedValue1", "watchedValue2", "watchedValue3", "watchedValue4",
"status", "extra", "foreignKey", "objectGuid", "userData",
]
data = [
r for r in data
if any(term in str(r.get(field, "")).lower() for field in searchable)
]
return data
def apply_events_filters(data, options):
"""Filter a list of Events table rows.
Handles: eveMac, eventType, date range, column filters, free-text search.
"""
if not options:
return data
# MAC filter
if options.eveMac:
mac = options.eveMac.lower()
data = [r for r in data if str(r.get("eveMac", "")).lower() == mac]
# Event-type filter
if options.eventType:
et = options.eventType.lower()
data = [r for r in data if str(r.get("eveEventType", "")).lower() == et]
# Date-range filter on eveDateTime
if options.dateFrom:
data = [r for r in data if str(r.get("eveDateTime", "")) >= options.dateFrom]
if options.dateTo:
data = [r for r in data if str(r.get("eveDateTime", "")) <= options.dateTo]
# Column-value exact-match filters
if options.filters:
for f in options.filters:
if f.filterColumn and f.filterValue is not None:
data = [
r for r in data
if str(r.get(f.filterColumn, "")).lower() == str(f.filterValue).lower()
]
# Free-text search
if options.search:
term = options.search.lower()
searchable = ["eveMac", "eveIp", "eveEventType", "eveAdditionalInfo"]
data = [
r for r in data
if any(term in str(r.get(field, "")).lower() for field in searchable)
]
return data

View File

@@ -0,0 +1,261 @@
import graphene # noqa: F401 (re-exported for schema creation in graphql_endpoint.py)
from graphene import (
ObjectType, String, Int, Boolean, List, InputObjectType,
)
# ---------------------------------------------------------------------------
# Shared 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)
# ---------------------------------------------------------------------------
# Devices
# ---------------------------------------------------------------------------
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
# ---------------------------------------------------------------------------
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")
# ---------------------------------------------------------------------------
# Language Strings
# ---------------------------------------------------------------------------
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")
# ---------------------------------------------------------------------------
# Plugin tables (Plugins_Objects, Plugins_Events, Plugins_History)
# All three tables share the same schema — one ObjectType, three result wrappers.
# GraphQL requires distinct named types even when fields are identical.
# ---------------------------------------------------------------------------
class PluginQueryOptionsInput(InputObjectType):
page = Int()
limit = Int()
sort = List(SortOptionsInput)
search = String()
filters = List(FilterOptionsInput)
plugin = String(description="Filter by plugin prefix (e.g. 'ARPSCAN')")
foreignKey = String(description="Filter by foreignKey (e.g. device MAC)")
dateFrom = String(description="dateTimeCreated >= dateFrom (ISO datetime string)")
dateTo = String(description="dateTimeCreated <= dateTo (ISO datetime string)")
class PluginEntry(ObjectType):
index = Int(description="Auto-increment primary key")
plugin = String(description="Plugin prefix identifier")
objectPrimaryId = String(description="Primary identifier (e.g. MAC, IP)")
objectSecondaryId = String(description="Secondary identifier")
dateTimeCreated = String(description="Record creation timestamp")
dateTimeChanged = String(description="Record last-changed timestamp")
watchedValue1 = String(description="Monitored value 1")
watchedValue2 = String(description="Monitored value 2")
watchedValue3 = String(description="Monitored value 3")
watchedValue4 = String(description="Monitored value 4")
status = String(description="Record status")
extra = String(description="Extra JSON payload")
userData = String(description="User-supplied data")
foreignKey = String(description="Foreign key (e.g. device MAC)")
syncHubNodeName = String(description="Sync hub node name")
helpVal1 = String(description="Helper value 1")
helpVal2 = String(description="Helper value 2")
helpVal3 = String(description="Helper value 3")
helpVal4 = String(description="Helper value 4")
objectGuid = String(description="Object GUID")
class PluginsObjectsResult(ObjectType):
entries = List(PluginEntry, description="Plugins_Objects rows")
count = Int(description="Filtered count (before pagination)")
db_count = Int(description="Total rows in table before any filter")
class PluginsEventsResult(ObjectType):
entries = List(PluginEntry, description="Plugins_Events rows")
count = Int(description="Filtered count (before pagination)")
db_count = Int(description="Total rows in table before any filter")
class PluginsHistoryResult(ObjectType):
entries = List(PluginEntry, description="Plugins_History rows")
count = Int(description="Filtered count (before pagination)")
db_count = Int(description="Total rows in table before any filter")
# ---------------------------------------------------------------------------
# Events table (device presence events)
# ---------------------------------------------------------------------------
class EventQueryOptionsInput(InputObjectType):
page = Int()
limit = Int()
sort = List(SortOptionsInput)
search = String()
filters = List(FilterOptionsInput)
eveMac = String(description="Filter by device MAC address")
eventType = String(description="Filter by eveEventType (exact match)")
dateFrom = String(description="eveDateTime >= dateFrom (ISO datetime string)")
dateTo = String(description="eveDateTime <= dateTo (ISO datetime string)")
class EventEntry(ObjectType):
rowid = Int(description="SQLite rowid")
eveMac = String(description="Device MAC address")
eveIp = String(description="Device IP at event time")
eveDateTime = String(description="Event timestamp")
eveEventType = String(description="Event type (Connected, New Device, etc.)")
eveAdditionalInfo = String(description="Additional event info")
evePendingAlertEmail = Int(description="Pending alert flag (0 or 1)")
evePairEventRowid = Int(description="Paired event rowid (for session pairing)")
class EventsResult(ObjectType):
entries = List(EventEntry, description="Events table rows")
count = Int(description="Filtered count (before pagination)")
db_count = Int(description="Total rows in table before any filter")

View File

@@ -295,10 +295,16 @@ def get_device_sessions(mac, period):
return jsonify({"success": True, "sessions": sessions})
def get_session_events(event_type, period_date):
def get_session_events(event_type, period_date, page=1, limit=100, search=None, sort_col=0, sort_dir="desc"):
"""
Fetch events or sessions based on type and period.
Supports server-side pagination (page/limit), free-text search, and sorting.
Returns { data, total, recordsFiltered } so callers can drive DataTables serverSide mode.
"""
_MAX_LIMIT = 1000
limit = min(max(1, int(limit)), _MAX_LIMIT)
page = max(1, int(page))
conn = get_temp_db_connection()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
@@ -420,4 +426,30 @@ def get_session_events(event_type, period_date):
table_data["data"].append(row)
return jsonify(table_data)
all_rows = table_data["data"]
# --- Sorting ---
num_cols = len(all_rows[0]) if all_rows else 0
if 0 <= sort_col < num_cols:
reverse = sort_dir.lower() == "desc"
all_rows.sort(
key=lambda r: (r[sort_col] is None, r[sort_col] if r[sort_col] is not None else ""),
reverse=reverse,
)
total = len(all_rows)
# --- Free-text search (applied after formatting so display values are searchable) ---
if search:
search_lower = search.strip().lower()
def _row_matches(r):
return any(search_lower in str(v).lower() for v in r if v is not None)
all_rows = [r for r in all_rows if _row_matches(r)]
records_filtered = len(all_rows)
# --- Pagination ---
offset = (page - 1) * limit
paged_rows = all_rows[offset: offset + limit]
return jsonify({"data": paged_rows, "total": total, "recordsFiltered": records_filtered})