Skip to content

Commit ec3e4c8

Browse files
committed
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.
1 parent 250e533 commit ec3e4c8

15 files changed

+1312
-352
lines changed

CONTRIBUTING.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ Before opening a new issue:
1515
- [Check Common Issues & Debug Tips](https://docs.netalertx.com/DEBUG_TIPS#common-issues)
1616
- [Search Closed Issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
1717

18+
---
19+
20+
## Use of AI
21+
22+
Use of AI-assisted tools is permitted, provided all generated code is reviewed, understood, and verified before submission.
23+
24+
- All AI-generated code must meet the project's **quality, security, and performance standards**.
25+
- Contributors are responsible for **fully understanding** any code they submit, regardless of how it was produced.
26+
- Prefer **clarity and maintainability over cleverness or brevity**. Readable code is always favored over dense or obfuscated implementations.
27+
- Follow the **DRY (Don't Repeat Yourself) principle** where appropriate, without sacrificing readability.
28+
- Do not submit code that you cannot confidently explain or debug.
29+
30+
All changes must pass the **full test suite** before opening a PR.
31+
32+
1833
---
1934

2035
## Submitting Pull Requests (PRs)
@@ -28,11 +43,19 @@ Please:
2843
- Provide a clear title and description for your PR
2944
- If relevant, add or update tests and documentation
3045
- For plugins, refer to the [Plugin Dev Guide](https://docs.netalertx.com/PLUGINS_DEV)
46+
- Switch the PR to DRAFT mode if still being worked on
47+
- Keep PRs **focused and minimal** — avoid unrelated changes in a single PR
48+
- PRs that do not meet these guidelines may be closed without review
49+
50+
## Commit Messages
3151

52+
- Use clear, descriptive commit messages
53+
- Explain *why* a change was made, not just *what* changed
54+
- Reference related issues where applicable
3255

33-
## Code quality
56+
## Code Quality
3457

35-
- read and follow the [code-standards](/.github/skills/code-standards/SKILL.md)
58+
- Read and follow the [code standards](/.github/skills/code-standards/SKILL.md)
3659

3760
---
3861

docs/API_GRAPHQL.md

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ GraphQL queries are **read-optimized for speed**. Data may be slightly out of da
44

55
* Devices
66
* Settings
7+
* Events
8+
* PluginsObjects
9+
* PluginsHistory
10+
* PluginsEvents
711
* Language Strings (LangStrings)
812

913
## Endpoints
@@ -254,11 +258,160 @@ curl 'http://host:GRAPHQL_PORT/graphql' \
254258

255259
---
256260

261+
## Plugin Tables (Objects, Events, History)
262+
263+
Three queries expose the plugin database tables with server-side pagination, filtering, and search:
264+
265+
* `pluginsObjects` — current plugin object state
266+
* `pluginsEvents` — unprocessed plugin events
267+
* `pluginsHistory` — historical plugin event log
268+
269+
All three share the same `PluginQueryOptionsInput` and return the same `PluginEntry` shape.
270+
271+
### Sample Query
272+
273+
```graphql
274+
query GetPluginObjects($options: PluginQueryOptionsInput) {
275+
pluginsObjects(options: $options) {
276+
dbCount
277+
count
278+
entries {
279+
index plugin objectPrimaryId objectSecondaryId
280+
dateTimeCreated dateTimeChanged
281+
watchedValue1 watchedValue2 watchedValue3 watchedValue4
282+
status extra userData foreignKey
283+
syncHubNodeName helpVal1 helpVal2 helpVal3 helpVal4 objectGuid
284+
}
285+
}
286+
}
287+
```
288+
289+
### Query Parameters (`PluginQueryOptionsInput`)
290+
291+
| Parameter | Type | Description |
292+
| ------------ | ----------------- | ------------------------------------------------------ |
293+
| `page` | Int | Page number (1-based). |
294+
| `limit` | Int | Rows per page (max 1000). |
295+
| `sort` | [SortOptionsInput] | Sorting options (`field`, `order`). |
296+
| `search` | String | Free-text search across key columns. |
297+
| `filters` | [FilterOptionsInput] | Column-value exact-match filters. |
298+
| `plugin` | String | Plugin prefix to scope results (e.g. `"ARPSCAN"`). |
299+
| `foreignKey` | String | Foreign key filter (e.g. device MAC). |
300+
| `dateFrom` | String | Start of date range filter on `dateTimeCreated`. |
301+
| `dateTo` | String | End of date range filter on `dateTimeCreated`. |
302+
303+
### Response Fields
304+
305+
| Field | Type | Description |
306+
| --------- | ------------- | ------------------------------------------------------------- |
307+
| `dbCount` | Int | Total rows for the requested plugin (before search/filters). |
308+
| `count` | Int | Total rows after all filters (before pagination). |
309+
| `entries` | [PluginEntry] | Paginated list of plugin entries. |
310+
311+
### `curl` Example
312+
313+
```sh
314+
curl 'http://host:GRAPHQL_PORT/graphql' \
315+
-X POST \
316+
-H 'Authorization: Bearer API_TOKEN' \
317+
-H 'Content-Type: application/json' \
318+
--data '{
319+
"query": "query GetPluginObjects($options: PluginQueryOptionsInput) { pluginsObjects(options: $options) { dbCount count entries { index plugin objectPrimaryId status foreignKey } } }",
320+
"variables": {
321+
"options": {
322+
"plugin": "ARPSCAN",
323+
"page": 1,
324+
"limit": 25
325+
}
326+
}
327+
}'
328+
```
329+
330+
### Badge Prefetch (Batched Counts)
331+
332+
Use GraphQL aliases to fetch counts for all plugins in a single request:
333+
334+
```graphql
335+
query BadgeCounts {
336+
ARPSCAN: pluginsObjects(options: {plugin: "ARPSCAN", page: 1, limit: 1}) { dbCount }
337+
INTRNT: pluginsObjects(options: {plugin: "INTRNT", page: 1, limit: 1}) { dbCount }
338+
}
339+
```
340+
341+
---
342+
343+
## Events Query
344+
345+
Access the Events table with server-side pagination, filtering, and search.
346+
347+
### Sample Query
348+
349+
```graphql
350+
query GetEvents($options: EventQueryOptionsInput) {
351+
events(options: $options) {
352+
dbCount
353+
count
354+
entries {
355+
eveMac
356+
eveIp
357+
eveDateTime
358+
eveEventType
359+
eveAdditionalInfo
360+
evePendingAlertEmail
361+
}
362+
}
363+
}
364+
```
365+
366+
### Query Parameters (`EventQueryOptionsInput`)
367+
368+
| Parameter | Type | Description |
369+
| ----------- | ------------------ | ------------------------------------------------ |
370+
| `page` | Int | Page number (1-based). |
371+
| `limit` | Int | Rows per page (max 1000). |
372+
| `sort` | [SortOptionsInput] | Sorting options (`field`, `order`). |
373+
| `search` | String | Free-text search across key columns. |
374+
| `filters` | [FilterOptionsInput] | Column-value exact-match filters. |
375+
| `eveMac` | String | Filter by device MAC address. |
376+
| `eventType` | String | Filter by event type (e.g. `"New Device"`). |
377+
| `dateFrom` | String | Start of date range filter on `eveDateTime`. |
378+
| `dateTo` | String | End of date range filter on `eveDateTime`. |
379+
380+
### Response Fields
381+
382+
| Field | Type | Description |
383+
| --------- | ------------ | ------------------------------------------------------------ |
384+
| `dbCount` | Int | Total rows in the Events table (before any filters). |
385+
| `count` | Int | Total rows after all filters (before pagination). |
386+
| `entries` | [EventEntry] | Paginated list of event entries. |
387+
388+
### `curl` Example
389+
390+
```sh
391+
curl 'http://host:GRAPHQL_PORT/graphql' \
392+
-X POST \
393+
-H 'Authorization: Bearer API_TOKEN' \
394+
-H 'Content-Type: application/json' \
395+
--data '{
396+
"query": "query GetEvents($options: EventQueryOptionsInput) { events(options: $options) { dbCount count entries { eveMac eveIp eveDateTime eveEventType } } }",
397+
"variables": {
398+
"options": {
399+
"eveMac": "00:11:22:33:44:55",
400+
"page": 1,
401+
"limit": 50
402+
}
403+
}
404+
}'
405+
```
406+
407+
---
408+
257409
## Notes
258410

259-
* Device, settings, and LangStrings queries can be combined in **one request** since GraphQL supports batching.
411+
* Device, settings, LangStrings, plugin, and event queries can be combined in **one request** since GraphQL supports batching.
260412
* The `fallback_to_en` feature ensures UI always has a value even if a translation is missing.
261413
* Data is **cached in memory** per JSON file; changes to language or plugin files will only refresh after the cache detects a file modification.
262414
* The `setOverriddenByEnv` flag helps identify setting values that are locked at container runtime.
263-
* The schema is **read-only** — updates must be performed through other APIs or configuration management. See the other [API](API.md) endpoints for details.
415+
* Plugin queries scope `dbCount` to the requested `plugin`/`foreignKey` so badge counts reflect per-plugin totals.
416+
* The schema is **read-only** — updates must be performed through other APIs or configuration management. See the other [API](API.md) endpoints for details.
264417

docs/API_SESSIONS.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,33 @@ curl -X GET "http://<server_ip>:<GRAPHQL_PORT>/sessions/AA:BB:CC:DD:EE:FF?period
224224
* `type` → Event type (`all`, `sessions`, `missing`, `voided`, `new`, `down`)
225225
Default: `all`
226226
* `period` → Period to retrieve events (`7 days`, `1 month`, etc.)
227+
* `page` → Page number, 1-based (default: `1`)
228+
* `limit` → Rows per page, max 1000 (default: `100`)
229+
* `search` → Free-text search filter across all columns
230+
* `sortCol` → Column index to sort by, 0-based (default: `0`)
231+
* `sortDir` → Sort direction: `asc` or `desc` (default: `desc`)
227232

228233
**Example:**
229234

230235
```
231-
/sessions/session-events?type=all&period=7 days
236+
/sessions/session-events?type=all&period=7 days&page=1&limit=25&sortCol=3&sortDir=desc
232237
```
233238

234239
**Response:**
235-
Returns a list of events or sessions with formatted connection, disconnection, duration, and IP information.
240+
241+
```json
242+
{
243+
"data": [...],
244+
"total": 150,
245+
"recordsFiltered": 150
246+
}
247+
```
248+
249+
| Field | Type | Description |
250+
| ----------------- | ---- | ------------------------------------------------- |
251+
| `data` | list | Paginated rows (each row is a list of values). |
252+
| `total` | int | Total rows before search filter. |
253+
| `recordsFiltered` | int | Total rows after search filter (before paging). |
236254

237255
#### `curl` Example
238256

front/deviceDetailsEvents.php

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,51 +32,64 @@
3232

3333
function loadEventsData() {
3434
const hideConnections = $('#chkHideConnectionEvents')[0].checked;
35-
const hideConnectionsStr = hideConnections ? 'true' : 'false';
3635

3736
let period = $("#period").val();
3837
let { start, end } = getPeriodStartEnd(period);
3938

40-
const rawSql = `
41-
SELECT eveDateTime, eveEventType, eveIp, eveAdditionalInfo
42-
FROM Events
43-
WHERE eveMac = "${mac}"
44-
AND eveDateTime BETWEEN "${start}" AND "${end}"
45-
AND (
46-
(eveEventType NOT IN ("Connected", "Disconnected", "VOIDED - Connected", "VOIDED - Disconnected"))
47-
OR "${hideConnectionsStr}" = "false"
48-
)
39+
const apiToken = getSetting("API_TOKEN");
40+
const apiBase = getApiBase();
41+
const graphqlUrl = `${apiBase}/graphql`;
42+
43+
const query = `
44+
query Events($options: EventQueryOptionsInput) {
45+
events(options: $options) {
46+
count
47+
entries {
48+
eveDateTime
49+
eveEventType
50+
eveIp
51+
eveAdditionalInfo
52+
}
53+
}
54+
}
4955
`;
5056

51-
const apiToken = getSetting("API_TOKEN");
52-
53-
const apiBaseUrl = getApiBase();
54-
const url = `${apiBaseUrl}/dbquery/read`;
55-
5657
$.ajax({
57-
url: url,
58+
url: graphqlUrl,
5859
method: "POST",
5960
contentType: "application/json",
6061
headers: {
6162
"Authorization": `Bearer ${apiToken}`
6263
},
6364
data: JSON.stringify({
64-
rawSql: btoa(rawSql)
65+
query,
66+
variables: {
67+
options: {
68+
eveMac: mac,
69+
dateFrom: start,
70+
dateTo: end,
71+
limit: 500,
72+
sort: [{ field: "eveDateTime", order: "desc" }]
73+
}
74+
}
6575
}),
6676
success: function (data) {
67-
// assuming read_query returns rows directly
68-
const rows = data["results"].map(row => {
69-
const rawDate = row.eveDateTime;
70-
const formattedDate = rawDate ? localizeTimestamp(rawDate) : '-';
71-
72-
return [
73-
formattedDate,
74-
row.eveDateTime,
75-
row.eveEventType,
76-
row.eveIp,
77-
row.eveAdditionalInfo
78-
];
79-
});
77+
const CONNECTION_TYPES = ["Connected", "Disconnected", "VOIDED - Connected", "VOIDED - Disconnected"];
78+
79+
const rows = data.data.events.entries
80+
.filter(row => !hideConnections || !CONNECTION_TYPES.includes(row.eveEventType))
81+
.map(row => {
82+
const rawDate = row.eveDateTime;
83+
const formattedDate = rawDate ? localizeTimestamp(rawDate) : '-';
84+
85+
return [
86+
formattedDate,
87+
row.eveDateTime,
88+
row.eveEventType,
89+
row.eveIp,
90+
row.eveAdditionalInfo
91+
];
92+
});
8093

8194
const table = $('#tableEvents').DataTable();
8295
table.clear();

0 commit comments

Comments
 (0)