@furystack/rest 5.0.8 → 5.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,9 @@
1
- export declare const tryDecodeQueryParam: (queryParam: any) => any;
1
+ /**
2
+ *
3
+ * Decoding steps: See the encoding steps in reverse order
4
+ * @param value The value to decode
5
+ * @returns The decoded value
6
+ */
7
+ export declare const decode: <T>(value: string) => T;
2
8
  export declare const deserializeQueryString: (fullQueryString: string) => any;
3
9
  //# sourceMappingURL=deserialize-query-string.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deserialize-query-string.d.ts","sourceRoot":"","sources":["../src/deserialize-query-string.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,eAAgB,GAAG,QAclD,CAAA;AAED,eAAO,MAAM,sBAAsB,oBAAqB,MAAM,QAiC7D,CAAA"}
1
+ {"version":3,"file":"deserialize-query-string.d.ts","sourceRoot":"","sources":["../src/deserialize-query-string.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,MAAM,aAAc,MAAM,MAAiF,CAAA;AAExH,eAAO,MAAM,sBAAsB,oBAAqB,MAAM,QAM7D,CAAA"}
@@ -1,43 +1,14 @@
1
- export const tryDecodeQueryParam = (queryParam) => {
2
- try {
3
- return JSON.parse(decodeURIComponent(queryParam.toString()));
4
- }
5
- catch {
6
- try {
7
- return JSON.parse(queryParam.toString());
8
- }
9
- catch {
10
- try {
11
- return decodeURIComponent(queryParam.toString());
12
- }
13
- catch {
14
- return queryParam;
15
- }
16
- }
17
- }
18
- };
1
+ /**
2
+ *
3
+ * Decoding steps: See the encoding steps in reverse order
4
+ * @param value The value to decode
5
+ * @returns The decoded value
6
+ */
7
+ export const decode = (value) => JSON.parse(decodeURIComponent(escape(atob(decodeURIComponent(value)))));
19
8
  export const deserializeQueryString = (fullQueryString) => {
20
- const queryString = fullQueryString?.replace?.('?', ''); // trim starting ?
21
- if (!queryString) {
22
- return {};
23
- }
24
- const entries = queryString
25
- .split('&')
26
- .map((value) => value.split('='))
27
- .filter(([key, value]) => key?.length && value?.length); // filter out empty keys
28
- const dedupedValues = entries
29
- .reduce((prev, current) => {
30
- const currentKey = current[0];
31
- const currentValue = tryDecodeQueryParam(current[1]);
32
- const existing = prev.find(([key]) => key === currentKey);
33
- if (existing) {
34
- existing[1] instanceof Array ? existing[1].push(currentValue) : (existing[1] = currentValue);
35
- return [...prev];
36
- }
37
- const newValue = [currentKey, currentKey.includes('[]') ? [currentValue] : currentValue];
38
- return [...prev, newValue];
39
- }, [])
40
- .map(([key, value]) => [key.replace('[]', ''), value]);
41
- return Object.fromEntries(dedupedValues);
9
+ const params = [...new URLSearchParams(fullQueryString).entries()]
10
+ .filter(([key, value]) => key && value)
11
+ .map(([key, value]) => [key, decode(value)]);
12
+ return Object.fromEntries(params);
42
13
  };
43
14
  //# sourceMappingURL=deserialize-query-string.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deserialize-query-string.js","sourceRoot":"","sources":["../src/deserialize-query-string.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,UAAe,EAAE,EAAE;IACrD,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAE,UAAkB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;KACtE;IAAC,MAAM;QACN,IAAI;YACF,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAA;SACzC;QAAC,MAAM;YACN,IAAI;gBACF,OAAO,kBAAkB,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAA;aACjD;YAAC,MAAM;gBACN,OAAO,UAAU,CAAA;aAClB;SACF;KACF;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,eAAuB,EAAE,EAAE;IAChE,MAAM,WAAW,GAAG,eAAe,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA,CAAC,kBAAkB;IAE1E,IAAI,CAAC,WAAW,EAAE;QAChB,OAAO,EAAE,CAAA;KACV;IAED,MAAM,OAAO,GAAG,WAAW;SACxB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;SAChC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,CAAC,CAAA,CAAC,wBAAwB;IAElF,MAAM,aAAa,GAAG,OAAO;SAC1B,MAAM,CACL,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;QACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,CAAC,CAAA;QACzD,IAAI,QAAQ,EAAE;YACZ,QAAQ,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAA;YAC5F,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;SACjB;QACD,MAAM,QAAQ,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAGtF,CAAA;QACD,OAAO,CAAC,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAA;IAC5B,CAAC,EACD,EAAwC,CACzC;SACA,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;IAExD,OAAO,MAAM,CAAC,WAAW,CAAC,aAAa,CAAC,CAAA;AAC1C,CAAC,CAAA"}
1
+ {"version":3,"file":"deserialize-query-string.js","sourceRoot":"","sources":["../src/deserialize-query-string.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAI,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAM,CAAA;AAExH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,eAAuB,EAAE,EAAE;IAChE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,eAAe,CAAC,eAAe,CAAC,CAAC,OAAO,EAAE,CAAC;SAC/D,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAE9C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;AACnC,CAAC,CAAA"}
@@ -1,3 +1,13 @@
1
- export declare const serializeValue: ([key, value]: [key: string, value: any]) => string;
2
- export declare const serializeToQueryString: <T extends object>(query: T) => string;
1
+ /**
2
+ * Serialize steps:
3
+ * 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}'
4
+ * 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D'
5
+ * 3. Unescape the URI Component, e.g.: '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' => '{"foo":"barð\x9F\x98\x89"}' - This and the first encodeURIComponent is needed because btoa only supports ASCII characters. We also don't want to encode the whole JSON string to keep a reasonable string length
6
+ * 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0='
7
+ * 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D'
8
+ * @param value The value to encode
9
+ * @returns The encoded value that can be used as an URL search parameter
10
+ */
11
+ export declare const serializeValue: (value: any) => string;
12
+ export declare const serializeToQueryString: <T extends object>(queryObject: T) => string;
3
13
  //# sourceMappingURL=serialize-to-query-string.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serialize-to-query-string.d.ts","sourceRoot":"","sources":["../src/serialize-to-query-string.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,qDAU1B,CAAA;AAED,eAAO,MAAM,sBAAsB,kCAAiC,MAKnE,CAAA"}
1
+ {"version":3,"file":"serialize-to-query-string.d.ts","sourceRoot":"","sources":["../src/serialize-to-query-string.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,UAAW,GAAG,WACsC,CAAA;AAE/E,eAAO,MAAM,sBAAsB,wCAAuC,MAQzE,CAAA"}
@@ -1,18 +1,17 @@
1
- export const serializeValue = ([key, value]) => {
2
- if (typeof value === 'object') {
3
- if (value instanceof Array) {
4
- if (!value.some((v) => typeof v === 'object')) {
5
- return value.map((val) => `${key}[]=${encodeURIComponent(val)}`).join('&');
6
- }
7
- }
8
- return `${key}=${encodeURIComponent(JSON.stringify(value))}`;
9
- }
10
- return `${key}=${encodeURIComponent(value)}`;
11
- };
12
- export const serializeToQueryString = (query) => {
13
- return Object.entries(query)
1
+ /**
2
+ * Serialize steps:
3
+ * 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}'
4
+ * 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D'
5
+ * 3. Unescape the URI Component, e.g.: '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' => '{"foo":"barð\x9F\x98\x89"}' - This and the first encodeURIComponent is needed because btoa only supports ASCII characters. We also don't want to encode the whole JSON string to keep a reasonable string length
6
+ * 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0='
7
+ * 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D'
8
+ * @param value The value to encode
9
+ * @returns The encoded value that can be used as an URL search parameter
10
+ */
11
+ export const serializeValue = (value) => encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(value)))));
12
+ export const serializeToQueryString = (queryObject) => {
13
+ return new URLSearchParams(Object.fromEntries(Object.entries(queryObject)
14
14
  .filter(([, value]) => value !== undefined)
15
- .map(serializeValue)
16
- .join('&');
15
+ .map(([key, value]) => [key, serializeValue(value)]))).toString();
17
16
  };
18
17
  //# sourceMappingURL=serialize-to-query-string.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serialize-to-query-string.js","sourceRoot":"","sources":["../src/serialize-to-query-string.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,CAA4B,EAAE,EAAE;IACxE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QAC7B,IAAI,KAAK,YAAY,KAAK,EAAE;YAC1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE;gBAC7C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;aAC3E;SACF;QACD,OAAO,GAAG,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAA;KAC7D;IACD,OAAO,GAAG,GAAG,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC9C,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAmB,KAAQ,EAAU,EAAE;IAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;SAC1C,GAAG,CAAC,cAAc,CAAC;SACnB,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC,CAAA"}
1
+ {"version":3,"file":"serialize-to-query-string.js","sourceRoot":"","sources":["../src/serialize-to-query-string.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAU,EAAE,EAAE,CAC3C,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAmB,WAAc,EAAU,EAAE;IACjF,OAAO,IAAI,eAAe,CACxB,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;SAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CACvD,CACF,CAAC,QAAQ,EAAE,CAAA;AACd,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/rest",
3
- "version": "5.0.8",
3
+ "version": "5.0.10",
4
4
  "description": "Generic REST package",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,13 +37,13 @@
37
37
  },
38
38
  "homepage": "https://github.com/furystack/furystack",
39
39
  "dependencies": {
40
- "@furystack/core": "^12.0.8",
41
- "@furystack/inject": "^8.0.7"
40
+ "@furystack/core": "^12.0.9",
41
+ "@furystack/inject": "^8.0.8"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^20.5.0",
44
+ "@types/node": "^20.5.3",
45
45
  "typescript": "^5.1.6",
46
- "vitest": "^0.34.1"
46
+ "vitest": "^0.34.2"
47
47
  },
48
48
  "gitHead": "1045d854bfd8c475b7035471d130d401417a2321"
49
49
  }
@@ -1,42 +1,50 @@
1
1
  import { deserializeQueryString } from './deserialize-query-string'
2
- import { serializeToQueryString } from './serialize-to-query-string'
2
+ import { serializeToQueryString, serializeValue } from './serialize-to-query-string'
3
3
  import { describe, it, expect } from 'vitest'
4
4
 
5
5
  describe('deserializeQueryString', () => {
6
- it('Should serialize a null value', () => {
6
+ it('Should deserialize a null value', () => {
7
7
  expect(deserializeQueryString(null as any)).toEqual({})
8
8
  })
9
9
 
10
- it('Should serialize an undefined value', () => {
10
+ it('Should deserialize an undefined value', () => {
11
11
  expect(deserializeQueryString(undefined as any)).toEqual({})
12
12
  })
13
13
 
14
- it('Should serialize an empty string value', () => {
14
+ it('Should deserialize an empty string value', () => {
15
15
  expect(deserializeQueryString('')).toEqual({})
16
16
  })
17
17
 
18
- it('Should serialize a string value with no keys / values', () => {
18
+ it('Should deserialize a string value with no keys / values', () => {
19
19
  expect(deserializeQueryString('?')).toEqual({})
20
20
  })
21
21
 
22
- it('Should serialize a string with given value but empty key', () => {
22
+ it('Should deserialize a string with given value but empty key', () => {
23
23
  expect(deserializeQueryString('?=alma')).toEqual({})
24
24
  })
25
25
 
26
- it('Should serialize a string with given key but empty value', () => {
26
+ it('Should deserialize a string with given key but empty value', () => {
27
27
  expect(deserializeQueryString('?alma=')).toEqual({})
28
28
  })
29
29
 
30
- it('Should serialize a list of primitive values', () => {
31
- expect(deserializeQueryString('?foo=value&bar=2&baz=false')).toEqual({ foo: 'value', bar: 2, baz: false })
32
- })
33
-
34
- it('Should serialize an array', () => {
35
- expect(deserializeQueryString('?foo[]=value&foo[]=2&foo[]=false')).toEqual({ foo: ['value', 2, false] })
30
+ it('Should deserialize a list of primitive values', () => {
31
+ expect(
32
+ deserializeQueryString(`?foo=${serializeValue('value')}&bar=${serializeValue(2)}&baz=${serializeValue(false)}`),
33
+ ).toEqual({
34
+ foo: 'value',
35
+ bar: 2,
36
+ baz: false,
37
+ })
36
38
  })
37
39
 
38
40
  it('Should override a value if not specified as an array', () => {
39
- expect(deserializeQueryString('?foo=value&foo=2&foo=false&foo=bar')).toEqual({ foo: 'bar' })
41
+ expect(
42
+ deserializeQueryString(
43
+ `?foo=${serializeValue('value')}&foo=${serializeValue(2)}&foo=${serializeValue(false)}&foo=${serializeValue(
44
+ 'bar',
45
+ )}`,
46
+ ),
47
+ ).toEqual({ foo: 'bar' })
40
48
  })
41
49
 
42
50
  it('Should serialize and deserialize an object with primitives', () => {
@@ -60,6 +68,6 @@ describe('deserializeQueryString', () => {
60
68
  })
61
69
 
62
70
  it('Should deserialize escaped values', () => {
63
- expect(deserializeQueryString('?alma=asd%2F*-%40')).toEqual({ alma: 'asd/*-@' })
71
+ expect(deserializeQueryString(`?alma=${serializeValue('asd/*-@?')}`)).toEqual({ alma: 'asd/*-@?' })
64
72
  })
65
73
  })
@@ -1,50 +1,15 @@
1
- export const tryDecodeQueryParam = (queryParam: any) => {
2
- try {
3
- return JSON.parse(decodeURIComponent((queryParam as any).toString()))
4
- } catch {
5
- try {
6
- return JSON.parse(queryParam.toString())
7
- } catch {
8
- try {
9
- return decodeURIComponent(queryParam.toString())
10
- } catch {
11
- return queryParam
12
- }
13
- }
14
- }
15
- }
1
+ /**
2
+ *
3
+ * Decoding steps: See the encoding steps in reverse order
4
+ * @param value The value to decode
5
+ * @returns The decoded value
6
+ */
7
+ export const decode = <T>(value: string) => JSON.parse(decodeURIComponent(escape(atob(decodeURIComponent(value))))) as T
16
8
 
17
9
  export const deserializeQueryString = (fullQueryString: string) => {
18
- const queryString = fullQueryString?.replace?.('?', '') // trim starting ?
19
-
20
- if (!queryString) {
21
- return {}
22
- }
23
-
24
- const entries = queryString
25
- .split('&')
26
- .map((value) => value.split('='))
27
- .filter(([key, value]) => key?.length && value?.length) // filter out empty keys
28
-
29
- const dedupedValues = entries
30
- .reduce(
31
- (prev, current) => {
32
- const currentKey = current[0]
33
- const currentValue = tryDecodeQueryParam(current[1])
34
- const existing = prev.find(([key]) => key === currentKey)
35
- if (existing) {
36
- existing[1] instanceof Array ? existing[1].push(currentValue) : (existing[1] = currentValue)
37
- return [...prev]
38
- }
39
- const newValue = [currentKey, currentKey.includes('[]') ? [currentValue] : currentValue] as [
40
- string,
41
- string | string[],
42
- ]
43
- return [...prev, newValue]
44
- },
45
- [] as Array<[string, string | string[]]>,
46
- )
47
- .map(([key, value]) => [key.replace('[]', ''), value])
10
+ const params = [...new URLSearchParams(fullQueryString).entries()]
11
+ .filter(([key, value]) => key && value)
12
+ .map(([key, value]) => [key, decode(value)])
48
13
 
49
- return Object.fromEntries(dedupedValues)
14
+ return Object.fromEntries(params)
50
15
  }
@@ -3,25 +3,29 @@ import { describe, it, expect } from 'vitest'
3
3
 
4
4
  describe('serializeToQueryString', () => {
5
5
  it('Should serialize primitive values', () => {
6
- expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: 0, e: null })).toBe('a=1&b=false&c=foo&d=0&e=null')
6
+ expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: 0, e: null })).toMatchInlineSnapshot(
7
+ '"a=MQ%253D%253D&b=ZmFsc2U%253D&c=ImZvbyI%253D&d=MA%253D%253D&e=bnVsbA%253D%253D"',
8
+ )
7
9
  })
8
10
 
9
11
  it('Should exclude explicit undefined', () => {
10
- expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: undefined })).toBe('a=1&b=false&c=foo')
12
+ expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: undefined })).toMatchInlineSnapshot(
13
+ '"a=MQ%253D%253D&b=ZmFsc2U%253D&c=ImZvbyI%253D"',
14
+ )
11
15
  })
12
16
 
13
17
  it('Should serialize primitive arrays', () => {
14
- expect(serializeToQueryString({ array: [1, 2, 3, 4] })).toBe('array[]=1&array[]=2&array[]=3&array[]=4')
18
+ expect(serializeToQueryString({ array: [1, 2, 3, 4] })).toMatchInlineSnapshot('"array=WzEsMiwzLDRd"')
15
19
  })
16
20
 
17
21
  it('Should serialize objects', () => {
18
- expect(serializeToQueryString({ foo: { a: 1, b: 'value' } })).toBe(
19
- `foo=${encodeURIComponent('{"a":1,"b":"value"}')}`,
22
+ expect(serializeToQueryString({ foo: { a: 1, b: 'value' } })).toMatchInlineSnapshot(
23
+ '"foo=eyJhIjoxLCJiIjoidmFsdWUifQ%253D%253D"',
20
24
  )
21
25
  })
22
26
 
23
27
  it('Should serialize an array that contains a non-primitive entry', () => {
24
28
  const array = [1, 2, 3, { foo: 1 }]
25
- expect(serializeToQueryString({ array })).toBe(`array=${encodeURIComponent(JSON.stringify(array))}`)
29
+ expect(serializeToQueryString({ array })).toMatchInlineSnapshot('"array=WzEsMiwzLHsiZm9vIjoxfV0%253D"')
26
30
  })
27
31
  })
@@ -1,18 +1,22 @@
1
- export const serializeValue = ([key, value]: [key: string, value: any]) => {
2
- if (typeof value === 'object') {
3
- if (value instanceof Array) {
4
- if (!value.some((v) => typeof v === 'object')) {
5
- return value.map((val) => `${key}[]=${encodeURIComponent(val)}`).join('&')
6
- }
7
- }
8
- return `${key}=${encodeURIComponent(JSON.stringify(value))}`
9
- }
10
- return `${key}=${encodeURIComponent(value)}`
11
- }
1
+ /**
2
+ * Serialize steps:
3
+ * 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}'
4
+ * 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D'
5
+ * 3. Unescape the URI Component, e.g.: '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' => '{"foo":"barð\x9F\x98\x89"}' - This and the first encodeURIComponent is needed because btoa only supports ASCII characters. We also don't want to encode the whole JSON string to keep a reasonable string length
6
+ * 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0='
7
+ * 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D'
8
+ * @param value The value to encode
9
+ * @returns The encoded value that can be used as an URL search parameter
10
+ */
11
+ export const serializeValue = (value: any) =>
12
+ encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(value)))))
12
13
 
13
- export const serializeToQueryString = <T extends object>(query: T): string => {
14
- return Object.entries(query)
15
- .filter(([, value]) => value !== undefined)
16
- .map(serializeValue)
17
- .join('&')
14
+ export const serializeToQueryString = <T extends object>(queryObject: T): string => {
15
+ return new URLSearchParams(
16
+ Object.fromEntries(
17
+ Object.entries(queryObject)
18
+ .filter(([, value]) => value !== undefined)
19
+ .map(([key, value]) => [key, serializeValue(value)]),
20
+ ),
21
+ ).toString()
18
22
  }