Skip to content

Commit a5c3c1f

Browse files
fix: implement deep object serialization for nested filter parameters (#40)
* fix: implement deep object serialization for nested filter parameters - Fix issue where heavily nested filter arguments were converted to [object Object] - Add serializeDeepObject() method to properly handle nested query parameters - Support filter and proxy parameters with style: 'deepObject' and explode: true - Transform {filter: {updated_after: "2020-01-01"}} to filter[updated_after]=2020-01-01 - Add comprehensive test coverage for deep object serialization - Maintain backward compatibility for non-filter parameters Resolves #38 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: mattzcarey <mattzcarey@users.noreply.github.com> * feat: apply deep object serialization to all parameters - Remove restriction to only 'filter' and 'proxy' parameters - Apply deep object serialization to all object parameters consistently - Eliminates [object Object] conversion issues for nested parameters - Add comprehensive test coverage for mixed parameter types - Maintain backward compatibility for primitive values Co-authored-by: mattzcarey <mattzcarey@users.noreply.github.com> * feat: implement comprehensive security and performance improvements for parameter serialization - Add recursion depth protection (max 10 levels) to prevent stack overflow attacks - Implement circular reference detection using WeakSet for robust object handling - Add parameter key validation with strict regex to prevent injection attacks - Handle special types safely: Date (ISO), RegExp (string), reject functions - Optimize performance with batch parameter building and reduced allocations - Add comprehensive test coverage for security scenarios and edge cases - Maintain backward compatibility while addressing critical vulnerabilities Resolves security issues identified in code review including: - Stack overflow vulnerability (CVE-level risk) - Parameter injection attacks - Information disclosure through unsafe string conversion - Performance issues with large object processing Co-authored-by: mattzcarey <mattzcarey@users.noreply.github.com> * feat: add HRIS employee filters example demonstrating deep object serialization - Creates comprehensive example showing filter usage with HRIS list employees endpoint - Demonstrates date, email, and employee_number filtering capabilities - Shows proxy parameter usage for provider-specific filters - Tests deep object serialization with nested filter combinations - Validates proper URL encoding of OpenAPI deepObject style parameters - Includes edge case handling for empty filter objects - All 7 test scenarios pass, confirming deep object serialization works correctly Co-authored-by: mattzcarey <mattzcarey@users.noreply.github.com> * snapshots * filters example --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: mattzcarey <mattzcarey@users.noreply.github.com>
1 parent 7f29842 commit a5c3c1f

File tree

17 files changed

+4638
-12579
lines changed

17 files changed

+4638
-12579
lines changed

examples/filters.ts

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Filters Example
4+
*
5+
* This example demonstrates how to use filters with the HRIS list employees endpoint.
6+
* It showcases the deep object serialization implementation that properly converts
7+
* nested filter objects to OpenAPI deepObject style query parameters.
8+
*
9+
* Key features demonstrated:
10+
* 1. Basic filter usage (updated_after, email, employee_number)
11+
* 2. Proxy parameter usage for provider-specific filters
12+
* 3. Complex nested filter combinations
13+
* 4. Proper serialization of filter objects to query parameters
14+
*
15+
* Usage:
16+
*
17+
* ```bash
18+
* bun run examples/filters.ts
19+
* ```
20+
*/
21+
22+
import assert from 'node:assert';
23+
import { StackOneToolSet } from '../src';
24+
25+
type DryRunResult = { url: string };
26+
27+
const hriseEmployeeFilters = async (): Promise<void> => {
28+
// Initialize the toolset
29+
const toolset = new StackOneToolSet();
30+
const accountId = 'test-account-id';
31+
32+
// Get the HRIS tools with account ID
33+
const tools = toolset.getStackOneTools('hris_*', accountId);
34+
const employeesTool = tools.getTool('hris_list_employees');
35+
36+
assert(employeesTool !== undefined, 'Expected to find hris_list_employees tool');
37+
38+
console.log('🧪 Testing HRIS Employee Filters with Deep Object Serialization\n');
39+
40+
/*
41+
* Example 1: Basic date filter
42+
* Demonstrates filtering employees updated after a specific date
43+
*/
44+
console.log('1️⃣ Basic Date Filter Test');
45+
const basicDateFilter = (await employeesTool.execute(
46+
{
47+
filter: {
48+
updated_after: '2023-01-01T00:00:00.000Z',
49+
},
50+
},
51+
{ dryRun: true }
52+
)) as DryRunResult;
53+
54+
console.log('Filter object:', { filter: { updated_after: '2023-01-01T00:00:00.000Z' } });
55+
console.log('Serialized URL:', basicDateFilter.url);
56+
57+
// Verify that the filter is properly serialized as deepObject style
58+
assert(
59+
basicDateFilter.url.includes('filter%5Bupdated_after%5D=2023-01-01T00%3A00%3A00.000Z'),
60+
'Expected URL to contain properly serialized date filter'
61+
);
62+
console.log('✅ Date filter serialized correctly\n');
63+
64+
/*
65+
* Example 2: Email filter
66+
* Demonstrates filtering employees by email address
67+
*/
68+
console.log('2️⃣ Email Filter Test');
69+
const emailFilter = (await employeesTool.execute(
70+
{
71+
filter: {
72+
email: 'john.doe@company.com',
73+
},
74+
},
75+
{ dryRun: true }
76+
)) as DryRunResult;
77+
78+
console.log('Filter object:', { filter: { email: 'john.doe@company.com' } });
79+
console.log('Serialized URL:', emailFilter.url);
80+
81+
assert(
82+
emailFilter.url.includes('filter%5Bemail%5D=john.doe%40company.com'),
83+
'Expected URL to contain properly serialized email filter'
84+
);
85+
console.log('✅ Email filter serialized correctly\n');
86+
87+
/*
88+
* Example 3: Employee number filter
89+
* Demonstrates filtering employees by employee number
90+
*/
91+
console.log('3️⃣ Employee Number Filter Test');
92+
const employeeNumberFilter = (await employeesTool.execute(
93+
{
94+
filter: {
95+
employee_number: 'EMP001',
96+
},
97+
},
98+
{ dryRun: true }
99+
)) as DryRunResult;
100+
101+
console.log('Filter object:', { filter: { employee_number: 'EMP001' } });
102+
console.log('Serialized URL:', employeeNumberFilter.url);
103+
104+
assert(
105+
employeeNumberFilter.url.includes('filter%5Bemployee_number%5D=EMP001'),
106+
'Expected URL to contain properly serialized employee number filter'
107+
);
108+
console.log('✅ Employee number filter serialized correctly\n');
109+
110+
/*
111+
* Example 4: Multiple filters combined
112+
* Demonstrates using multiple filter parameters together
113+
*/
114+
console.log('4️⃣ Multiple Filters Combined Test');
115+
const multipleFilters = (await employeesTool.execute(
116+
{
117+
filter: {
118+
updated_after: '2023-06-01T00:00:00.000Z',
119+
email: 'jane.smith@company.com',
120+
employee_number: 'EMP002',
121+
},
122+
},
123+
{ dryRun: true }
124+
)) as DryRunResult;
125+
126+
console.log('Filter object:', {
127+
filter: {
128+
updated_after: '2023-06-01T00:00:00.000Z',
129+
email: 'jane.smith@company.com',
130+
employee_number: 'EMP002',
131+
},
132+
});
133+
console.log('Serialized URL:', (multipleFilters as { url: string }).url);
134+
135+
// Verify all filters are present in the URL
136+
assert(
137+
multipleFilters.url.includes('filter%5Bupdated_after%5D=2023-06-01T00%3A00%3A00.000Z'),
138+
'Expected URL to contain date filter'
139+
);
140+
assert(
141+
multipleFilters.url.includes('filter%5Bemail%5D=jane.smith%40company.com'),
142+
'Expected URL to contain email filter'
143+
);
144+
assert(
145+
multipleFilters.url.includes('filter%5Bemployee_number%5D=EMP002'),
146+
'Expected URL to contain employee number filter'
147+
);
148+
console.log('✅ Multiple filters serialized correctly\n');
149+
150+
/*
151+
* Example 5: Proxy parameters for provider-specific filtering
152+
* Demonstrates using proxy parameters which also use deepObject serialization
153+
*/
154+
console.log('5️⃣ Proxy Parameters Test');
155+
const proxyParameters = (await employeesTool.execute(
156+
{
157+
proxy: {
158+
custom_field: 'value123',
159+
provider_filter: {
160+
department: 'Engineering',
161+
status: 'active',
162+
},
163+
},
164+
},
165+
{ dryRun: true }
166+
)) as DryRunResult;
167+
168+
console.log('Proxy object:', {
169+
proxy: {
170+
custom_field: 'value123',
171+
provider_filter: {
172+
department: 'Engineering',
173+
status: 'active',
174+
},
175+
},
176+
});
177+
console.log('Serialized URL:', proxyParameters.url);
178+
179+
// Verify proxy parameters are properly serialized
180+
assert(
181+
proxyParameters.url.includes('proxy%5Bcustom_field%5D=value123'),
182+
'Expected URL to contain proxy custom_field parameter'
183+
);
184+
assert(
185+
proxyParameters.url.includes('proxy%5Bprovider_filter%5D%5Bdepartment%5D=Engineering'),
186+
'Expected URL to contain nested proxy department parameter'
187+
);
188+
assert(
189+
proxyParameters.url.includes('proxy%5Bprovider_filter%5D%5Bstatus%5D=active'),
190+
'Expected URL to contain nested proxy status parameter'
191+
);
192+
console.log('✅ Proxy parameters with nested objects serialized correctly\n');
193+
194+
/*
195+
* Example 6: Complex combined scenario
196+
* Demonstrates combining filters, proxy parameters, and other query parameters
197+
*/
198+
console.log('6️⃣ Complex Combined Scenario Test');
199+
const complexScenario = (await employeesTool.execute(
200+
{
201+
filter: {
202+
updated_after: '2023-09-01T00:00:00.000Z',
203+
email: 'admin@company.com',
204+
},
205+
proxy: {
206+
include_terminated: 'false',
207+
custom_sorting: {
208+
field: 'hire_date',
209+
order: 'desc',
210+
},
211+
},
212+
fields: 'id,first_name,last_name,email,hire_date',
213+
page_size: '50',
214+
},
215+
{ dryRun: true }
216+
)) as DryRunResult;
217+
218+
console.log('Complex parameters:', {
219+
filter: {
220+
updated_after: '2023-09-01T00:00:00.000Z',
221+
email: 'admin@company.com',
222+
},
223+
proxy: {
224+
include_terminated: 'false',
225+
custom_sorting: {
226+
field: 'hire_date',
227+
order: 'desc',
228+
},
229+
},
230+
fields: 'id,first_name,last_name,email,hire_date',
231+
page_size: '50',
232+
});
233+
console.log('Serialized URL:', complexScenario.url);
234+
235+
// Verify complex scenario serialization
236+
assert(
237+
complexScenario.url.includes('filter%5Bupdated_after%5D=2023-09-01T00%3A00%3A00.000Z'),
238+
'Expected URL to contain complex date filter'
239+
);
240+
assert(
241+
complexScenario.url.includes('filter%5Bemail%5D=admin%40company.com'),
242+
'Expected URL to contain complex email filter'
243+
);
244+
assert(
245+
complexScenario.url.includes('proxy%5Binclude_terminated%5D=false'),
246+
'Expected URL to contain proxy boolean parameter'
247+
);
248+
assert(
249+
complexScenario.url.includes('proxy%5Bcustom_sorting%5D%5Bfield%5D=hire_date'),
250+
'Expected URL to contain nested proxy field parameter'
251+
);
252+
assert(
253+
complexScenario.url.includes('proxy%5Bcustom_sorting%5D%5Border%5D=desc'),
254+
'Expected URL to contain nested proxy order parameter'
255+
);
256+
assert(
257+
complexScenario.url.includes('fields=id%2Cfirst_name%2Clast_name%2Cemail%2Chire_date'),
258+
'Expected URL to contain fields parameter'
259+
);
260+
assert(
261+
complexScenario.url.includes('page_size=50'),
262+
'Expected URL to contain page_size parameter'
263+
);
264+
console.log('✅ Complex combined scenario serialized correctly\n');
265+
266+
/*
267+
* Example 7: Edge case - Empty filter objects
268+
* Demonstrates handling of empty filter objects
269+
*/
270+
console.log('7️⃣ Edge Case - Empty Filter Objects Test');
271+
const emptyFilterTest = (await employeesTool.execute(
272+
{
273+
filter: {},
274+
fields: 'id,first_name,last_name',
275+
},
276+
{ dryRun: true }
277+
)) as DryRunResult;
278+
279+
console.log('Empty filter object:', { filter: {}, fields: 'id,first_name,last_name' });
280+
console.log('Serialized URL:', emptyFilterTest.url);
281+
282+
// Verify that empty filter objects don't create problematic parameters
283+
assert(
284+
emptyFilterTest.url.includes('fields=id%2Cfirst_name%2Clast_name'),
285+
'Expected URL to contain fields parameter even with empty filter'
286+
);
287+
// Empty objects should not create parameters
288+
assert(
289+
!emptyFilterTest.url.includes('filter='),
290+
'Expected URL to not contain empty filter parameter'
291+
);
292+
console.log('✅ Empty filter objects handled correctly\n');
293+
};
294+
295+
// Run the example
296+
hriseEmployeeFilters();

0 commit comments

Comments
 (0)