Skip to content

Commit 7a9ec54

Browse files
authored
Merge pull request #8 from seamapi/array-param-support-get
2 parents 1087dcf + 2448503 commit 7a9ec54

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

src/lib/params-serializer.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import test from 'ava'
2+
3+
import {
4+
paramsSerializer,
5+
UnserializableParamError,
6+
} from './params-serializer.js'
7+
8+
test('serializes empty object', (t) => {
9+
t.is(paramsSerializer({}), '')
10+
})
11+
12+
test('serializes string', (t) => {
13+
t.is(paramsSerializer({ foo: 'd' }), 'foo=d')
14+
t.is(paramsSerializer({ foo: 'null' }), 'foo=null')
15+
t.is(paramsSerializer({ foo: 'undefined' }), 'foo=undefined')
16+
t.is(paramsSerializer({ foo: '0' }), 'foo=0')
17+
})
18+
19+
test('serializes number', (t) => {
20+
t.is(paramsSerializer({ foo: 1 }), 'foo=1')
21+
t.is(paramsSerializer({ foo: 23.8 }), 'foo=23.8')
22+
})
23+
24+
test('serializes boolean', (t) => {
25+
t.is(paramsSerializer({ foo: true }), 'foo=true')
26+
t.is(paramsSerializer({ foo: false }), 'foo=false')
27+
})
28+
29+
test('removes undefined params', (t) => {
30+
t.is(paramsSerializer({ bar: undefined }), '')
31+
t.is(paramsSerializer({ foo: 1, bar: undefined }), 'foo=1')
32+
})
33+
34+
test('removes null params', (t) => {
35+
t.is(paramsSerializer({ bar: null }), '')
36+
t.is(paramsSerializer({ foo: 1, bar: null }), 'foo=1')
37+
})
38+
39+
test('serializes empty array params', (t) => {
40+
t.is(paramsSerializer({ bar: [] }), 'bar=')
41+
t.is(paramsSerializer({ foo: 1, bar: [] }), 'bar=&foo=1')
42+
})
43+
44+
test('serializes array params with one value', (t) => {
45+
t.is(paramsSerializer({ bar: ['a'] }), 'bar=a')
46+
t.is(paramsSerializer({ foo: 1, bar: ['a'] }), 'bar=a&foo=1')
47+
})
48+
49+
test('serializes array params with many values', (t) => {
50+
t.is(paramsSerializer({ foo: 1, bar: ['a', '2'] }), 'bar=a&bar=2&foo=1')
51+
t.is(
52+
paramsSerializer({ foo: 1, bar: ['null', '2', 'undefined'] }),
53+
'bar=null&bar=2&bar=undefined&foo=1',
54+
)
55+
t.is(paramsSerializer({ foo: 1, bar: ['', '', ''] }), 'bar=&bar=&bar=&foo=1')
56+
t.is(
57+
paramsSerializer({ foo: 1, bar: ['', 'a', '2'] }),
58+
'bar=&bar=a&bar=2&foo=1',
59+
)
60+
t.is(
61+
paramsSerializer({ foo: 1, bar: ['', 'a', ''] }),
62+
'bar=&bar=a&bar=&foo=1',
63+
)
64+
})
65+
66+
test('cannot serialize single element array params with empty string', (t) => {
67+
t.throws(() => paramsSerializer({ foo: [''] }), {
68+
instanceOf: UnserializableParamError,
69+
})
70+
})
71+
72+
test('cannot serialize unserializable values', (t) => {
73+
t.throws(() => paramsSerializer({ foo: {} }), {
74+
instanceOf: UnserializableParamError,
75+
})
76+
t.throws(() => paramsSerializer({ foo: { x: 2 } }), {
77+
instanceOf: UnserializableParamError,
78+
})
79+
t.throws(() => paramsSerializer({ foo: () => {} }), {
80+
instanceOf: UnserializableParamError,
81+
})
82+
})
83+
84+
test('cannot serialize array params with unserializable values', (t) => {
85+
t.throws(() => paramsSerializer({ bar: ['a', null] }), {
86+
instanceOf: UnserializableParamError,
87+
})
88+
t.throws(() => paramsSerializer({ bar: ['a', undefined] }), {
89+
instanceOf: UnserializableParamError,
90+
})
91+
t.throws(() => paramsSerializer({ bar: ['a', ['s']] }), {
92+
instanceOf: UnserializableParamError,
93+
})
94+
t.throws(() => paramsSerializer({ bar: ['a', []] }), {
95+
instanceOf: UnserializableParamError,
96+
})
97+
t.throws(() => paramsSerializer({ bar: ['a', {}] }), {
98+
instanceOf: UnserializableParamError,
99+
})
100+
t.throws(() => paramsSerializer({ bar: ['a', { x: 2 }] }), {
101+
instanceOf: UnserializableParamError,
102+
})
103+
t.throws(() => paramsSerializer({ bar: ['a', () => {}] }), {
104+
instanceOf: UnserializableParamError,
105+
})
106+
})

src/lib/params-serializer.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { CustomParamsSerializer } from 'axios'
2+
3+
export const paramsSerializer: CustomParamsSerializer = (params) => {
4+
const searchParams = new URLSearchParams()
5+
6+
for (const [name, value] of Object.entries(params)) {
7+
if (value == null) continue
8+
9+
if (Array.isArray(value)) {
10+
if (value.length === 0) searchParams.set(name, '')
11+
if (value.length === 1 && value[0] === '') {
12+
throw new UnserializableParamError(
13+
name,
14+
`is a single element array containing the empty string which is unsupported because it serializes to the empty array`,
15+
)
16+
}
17+
for (const v of value) {
18+
throwIfUnserializable(name, v)
19+
searchParams.append(name, v)
20+
}
21+
continue
22+
}
23+
24+
throwIfUnserializable(name, value)
25+
searchParams.set(name, value)
26+
}
27+
28+
searchParams.sort()
29+
return searchParams.toString()
30+
}
31+
32+
const throwIfUnserializable = (k: string, v: unknown): void => {
33+
if (v == null) {
34+
throw new UnserializableParamError(k, `is ${v} or contains ${v}`)
35+
}
36+
37+
if (typeof v === 'function') {
38+
throw new UnserializableParamError(
39+
k,
40+
'is a function or contains a function',
41+
)
42+
}
43+
44+
if (typeof v === 'object') {
45+
throw new UnserializableParamError(k, 'is an object or contains an object')
46+
}
47+
}
48+
49+
export class UnserializableParamError extends Error {
50+
constructor(name: string, message: string) {
51+
super(`Could not serialize parameter: '${name}' ${message}`)
52+
this.name = this.constructor.name
53+
Error.captureStackTrace(this, this.constructor)
54+
}
55+
}

src/lib/seam/connect/axios.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import axios, { type Axios } from 'axios'
22

3+
import { paramsSerializer } from 'lib/params-serializer.js'
4+
35
import { getAuthHeaders } from './auth.js'
46
import {
57
isSeamHttpOptionsWithClient,
@@ -15,6 +17,7 @@ export const createAxiosClient = (
1517
return axios.create({
1618
baseURL: options.endpoint,
1719
withCredentials: isSeamHttpOptionsWithClientSessionToken(options),
20+
paramsSerializer,
1821
...options.axiosOptions,
1922
headers: {
2023
...getAuthHeaders(options),

0 commit comments

Comments
 (0)