Skip to content

Commit cea97b3

Browse files
committed
fix: tighten filter serialization and align tickets getAll with assets
Omit false boolean filter defaults, auto-enable pageinate when page_size or page_no is set, extract pagination helpers, and parse limited ticket responses using the same fallback chain as assets.
1 parent c7bd623 commit cea97b3

7 files changed

Lines changed: 154 additions & 33 deletions

File tree

nodes/HaloPSA/actions/tickets/getAll/execute.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { IExecuteFunctions } from 'n8n-workflow';
22
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
33
import { applyFiltersToQueryString, resolveFilters } from '../../../filterParameters';
44
import { apiRequest, apiRequestAllItems } from '../../../transport';
5-
import { HaloTicketsListResponse } from '../../Interfaces';
65

76
export async function execute(
87
this: IExecuteFunctions,
98
index: number,
109
): Promise<INodeExecutionData[]> {
1110
const returnAll = this.getNodeParameter('returnAll', index) as boolean;
1211
const filters = resolveFilters.call(this, index);
13-
12+
1413
const qs = applyFiltersToQueryString(filters);
1514

1615
if (!returnAll) {
@@ -22,12 +21,13 @@ export async function execute(
2221
const endpoint = '/Tickets';
2322
const body = {} as IDataObject;
2423

25-
if (returnAll) {
26-
const all = await apiRequestAllItems.call(this, requestMethod, endpoint, 'tickets', body, qs);
27-
return this.helpers.returnJsonArray(all);
28-
}
24+
if (returnAll) {
25+
const all = await apiRequestAllItems.call(this, requestMethod, endpoint, 'tickets', body, qs);
26+
return this.helpers.returnJsonArray(all);
27+
}
2928

30-
let responseData: HaloTicketsListResponse;
31-
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
32-
return this.helpers.returnJsonArray(responseData.tickets || []);
33-
}
29+
let responseData: any;
30+
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
31+
32+
return this.helpers.returnJsonArray(responseData.tickets || responseData || []);
33+
}

nodes/HaloPSA/filterParameters.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,7 @@ export function normalizeCustomfieldsField(fields: IDataObject): void {
165165

166166
/**
167167
* Whether a filter value should be sent to the HaloPSA API.
168-
* Omits UI defaults (0, '', empty arrays) that would over-constrain results.
169-
* Booleans are kept when explicitly set, including false.
168+
* Omits UI defaults (0, '', empty arrays, false booleans) that would over-constrain results.
170169
*/
171170
export function isMeaningfulFilterValue(value: unknown): boolean {
172171
if (value === undefined || value === null || value === '') {
@@ -175,12 +174,32 @@ export function isMeaningfulFilterValue(value: unknown): boolean {
175174
if (typeof value === 'number' && value === 0) {
176175
return false;
177176
}
177+
if (typeof value === 'boolean' && value === false) {
178+
return false;
179+
}
178180
if (Array.isArray(value) && value.length === 0) {
179181
return false;
180182
}
181183
return true;
182184
}
183185

186+
/**
187+
* HaloPSA silently ignores page_size/page_no unless pageinate=true (OpenAPI typo).
188+
* @see swagger.json GET /Tickets parameters page_size, page_no, pageinate
189+
*/
190+
export function applyHaloPaginationRules(qs: IDataObject): IDataObject {
191+
const result = { ...qs };
192+
const pageSize = result.page_size;
193+
const pageNo = result.page_no;
194+
if (
195+
(typeof pageSize === 'number' && pageSize > 0) ||
196+
(typeof pageNo === 'number' && pageNo > 0)
197+
) {
198+
result.pageinate = true;
199+
}
200+
return result;
201+
}
202+
184203
/** Applies common HaloPSA query-string transforms (e.g. multi-select custom fields). */
185204
export function applyFiltersToQueryString(filters: IDataObject): IDataObject {
186205
const qs: IDataObject = {};
@@ -201,5 +220,5 @@ export function applyFiltersToQueryString(filters: IDataObject): IDataObject {
201220
}
202221
}
203222

204-
return qs;
223+
return applyHaloPaginationRules(qs);
205224
}

nodes/HaloPSA/pagination.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/** Max records per non-paginated HaloPSA list request (matches common API usage). */
2+
export const NON_PAGINATED_MAX_COUNT = 1000;
3+
4+
/** HaloPSA paginated responses use page_size (max 100), not count. */
5+
export const PAGINATED_PAGE_SIZE = 100;
6+
7+
/** Whether a bulk fetch should continue with paginated requests. */
8+
export function shouldFetchMorePages(
9+
itemsLength: number,
10+
recordCount: number | undefined,
11+
maxCount = NON_PAGINATED_MAX_COUNT,
12+
): boolean {
13+
if (itemsLength < maxCount) {
14+
return false;
15+
}
16+
if (recordCount !== undefined && recordCount <= maxCount) {
17+
return false;
18+
}
19+
return true;
20+
}
21+
22+
/** First page number when continuing after a full non-paginated bulk fetch. */
23+
export function continuationPageNo(
24+
maxCount = NON_PAGINATED_MAX_COUNT,
25+
pageSize = PAGINATED_PAGE_SIZE,
26+
): number {
27+
return Math.floor(maxCount / pageSize) + 1;
28+
}
29+
30+
export function resolvePageSize(requested: unknown, maxPageSize = PAGINATED_PAGE_SIZE): number {
31+
if (typeof requested === 'number' && requested > 0) {
32+
return Math.min(requested, maxPageSize);
33+
}
34+
return maxPageSize;
35+
}

nodes/HaloPSA/transport.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {
1010
NodeApiError,
1111
} from 'n8n-workflow';
1212
import { extractResourceList, assertValidResourceListResponse, getRecordCount } from './resourceList';
13+
import {
14+
continuationPageNo,
15+
NON_PAGINATED_MAX_COUNT,
16+
resolvePageSize,
17+
shouldFetchMorePages,
18+
} from './pagination';
1319
import {
1420
clearCachedAccessToken,
1521
getCachedAccessToken,
@@ -198,12 +204,6 @@ export async function apiRequest(
198204
}
199205
}
200206

201-
/** Max records per non-paginated HaloPSA list request (matches common API usage). */
202-
const NON_PAGINATED_MAX_COUNT = 1000;
203-
204-
/** HaloPSA paginated responses use page_size (max 100), not count. */
205-
const PAGINATED_PAGE_SIZE = 100;
206-
207207
function omitManagedPaginationKeys(qs: IDataObject): IDataObject {
208208
const cleaned = { ...qs };
209209
delete cleaned.pageinate;
@@ -225,10 +225,7 @@ async function fetchPaginatedPages(
225225
const allItems: IDataObject[] = [];
226226
let page = startPage;
227227
let hasMorePages = true;
228-
const pageSize =
229-
typeof baseQs.page_size === 'number' && baseQs.page_size > 0
230-
? Math.min(baseQs.page_size as number, PAGINATED_PAGE_SIZE)
231-
: PAGINATED_PAGE_SIZE;
228+
const pageSize = resolvePageSize(baseQs.page_size);
232229

233230
while (hasMorePages) {
234231
const paginatedQs: IDataObject = {
@@ -271,24 +268,19 @@ export async function apiRequestAllItems(
271268
const items = extractResourceList(response, resourceKey);
272269
assertValidResourceListResponse(this.getNode(), response, items, resourceKey);
273270

274-
if (items.length < NON_PAGINATED_MAX_COUNT) {
275-
return items;
276-
}
277-
278271
const recordCount = getRecordCount(response);
279-
if (recordCount !== undefined && recordCount <= NON_PAGINATED_MAX_COUNT) {
272+
if (!shouldFetchMorePages(items.length, recordCount)) {
280273
return items;
281274
}
282275

283-
const startPage = Math.floor(NON_PAGINATED_MAX_COUNT / PAGINATED_PAGE_SIZE) + 1;
284276
const additionalItems = await fetchPaginatedPages(
285277
this,
286278
method,
287279
endpoint,
288280
resourceKey,
289281
body,
290282
baseQs,
291-
startPage,
283+
continuationPageNo(),
292284
);
293285

294286
return [...items, ...additionalItems];

test/filterParameters.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
22
import { describe, it } from 'node:test';
33
import {
44
applyFiltersToQueryString,
5+
applyHaloPaginationRules,
56
isMeaningfulFilterValue,
67
} from '../nodes/HaloPSA/filterParameters';
78

@@ -14,23 +15,38 @@ describe('isMeaningfulFilterValue', () => {
1415
assert.equal(isMeaningfulFilterValue([]), false);
1516
});
1617

17-
it('keeps meaningful values including false booleans', () => {
18-
assert.equal(isMeaningfulFilterValue(false), true);
18+
it('keeps true booleans and omits false booleans', () => {
19+
assert.equal(isMeaningfulFilterValue(false), false);
1920
assert.equal(isMeaningfulFilterValue(true), true);
2021
assert.equal(isMeaningfulFilterValue(40), true);
2122
assert.equal(isMeaningfulFilterValue('search text'), true);
2223
assert.equal(isMeaningfulFilterValue([1, 2]), true);
2324
});
2425
});
2526

27+
describe('applyHaloPaginationRules', () => {
28+
it('sets pageinate when page_size or page_no is present', () => {
29+
assert.deepEqual(applyHaloPaginationRules({ page_size: 50 }), {
30+
page_size: 50,
31+
pageinate: true,
32+
});
33+
assert.deepEqual(applyHaloPaginationRules({ page_no: 2 }), {
34+
page_no: 2,
35+
pageinate: true,
36+
});
37+
});
38+
});
39+
2640
describe('applyFiltersToQueryString', () => {
27-
it('passes open_only and requesttype_id while omitting zero defaults', () => {
41+
it('passes open_only and requesttype_id while omitting zero and false defaults', () => {
2842
const qs = applyFiltersToQueryString({
2943
open_only: true,
3044
requesttype_id: 40,
3145
agent_id: 0,
3246
status_id: 0,
3347
search: '',
48+
pageinate: false,
49+
default_columns: false,
3450
});
3551

3652
assert.deepEqual(qs, {
@@ -39,6 +55,17 @@ describe('applyFiltersToQueryString', () => {
3955
});
4056
});
4157

58+
it('forces pageinate when page_size is set in filters', () => {
59+
const qs = applyFiltersToQueryString({
60+
page_size: 25,
61+
});
62+
63+
assert.deepEqual(qs, {
64+
page_size: 25,
65+
pageinate: true,
66+
});
67+
});
68+
4269
it('joins multi-select arrays', () => {
4370
const qs = applyFiltersToQueryString({
4471
requesttype: [40, 41],

test/pagination.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
import {
4+
continuationPageNo,
5+
NON_PAGINATED_MAX_COUNT,
6+
PAGINATED_PAGE_SIZE,
7+
resolvePageSize,
8+
shouldFetchMorePages,
9+
} from '../nodes/HaloPSA/pagination';
10+
11+
describe('shouldFetchMorePages', () => {
12+
it('stops when bulk fetch is not full', () => {
13+
assert.equal(shouldFetchMorePages(50, 200), false);
14+
});
15+
16+
it('stops when record_count fits in bulk fetch', () => {
17+
assert.equal(shouldFetchMorePages(NON_PAGINATED_MAX_COUNT, 500), false);
18+
});
19+
20+
it('continues when bulk fetch is full and more records may exist', () => {
21+
assert.equal(shouldFetchMorePages(NON_PAGINATED_MAX_COUNT, 1500), true);
22+
assert.equal(shouldFetchMorePages(NON_PAGINATED_MAX_COUNT, undefined), true);
23+
});
24+
});
25+
26+
describe('continuationPageNo', () => {
27+
it('starts after the non-paginated bulk window', () => {
28+
assert.equal(continuationPageNo(), 11);
29+
assert.equal(continuationPageNo(1000, 100), 11);
30+
});
31+
});
32+
33+
describe('resolvePageSize', () => {
34+
it('caps page size at HaloPSA maximum', () => {
35+
assert.equal(resolvePageSize(250), PAGINATED_PAGE_SIZE);
36+
assert.equal(resolvePageSize(50), 50);
37+
assert.equal(resolvePageSize(undefined), PAGINATED_PAGE_SIZE);
38+
});
39+
});

test/resourceList.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ describe('extractResourceList', () => {
2323
const items = extractResourceList({ faults: [{ id: 99 }] }, 'tickets');
2424
assert.deepEqual(items, [{ id: 99 }]);
2525
});
26+
27+
it('reads a bare ticket array response', () => {
28+
const items = extractResourceList([{ id: 1 }, { id: 2 }], 'tickets');
29+
assert.equal(items.length, 2);
30+
});
31+
32+
it('returns empty when wrapped list key is missing', () => {
33+
assert.deepEqual(extractResourceList({ record_count: 5, tickets: null }, 'tickets'), []);
34+
});
2635
});
2736

2837
describe('getRecordCount', () => {

0 commit comments

Comments
 (0)