@furystack/rest
Advanced tools
Comparing version 5.0.9 to 5.0.10
@@ -1,3 +0,9 @@ | ||
export declare const tryDecodeQueryParam: (queryParam: any) => any; | ||
/** | ||
* | ||
* Decoding steps: See the encoding steps in reverse order | ||
* @param value The value to decode | ||
* @returns The decoded value | ||
*/ | ||
export declare const decode: <T>(value: string) => T; | ||
export declare const deserializeQueryString: (fullQueryString: string) => any; | ||
//# sourceMappingURL=deserialize-query-string.d.ts.map |
@@ -1,43 +0,14 @@ | ||
export const tryDecodeQueryParam = (queryParam) => { | ||
try { | ||
return JSON.parse(decodeURIComponent(queryParam.toString())); | ||
} | ||
catch { | ||
try { | ||
return JSON.parse(queryParam.toString()); | ||
} | ||
catch { | ||
try { | ||
return decodeURIComponent(queryParam.toString()); | ||
} | ||
catch { | ||
return queryParam; | ||
} | ||
} | ||
} | ||
}; | ||
/** | ||
* | ||
* Decoding steps: See the encoding steps in reverse order | ||
* @param value The value to decode | ||
* @returns The decoded value | ||
*/ | ||
export const decode = (value) => JSON.parse(decodeURIComponent(escape(atob(decodeURIComponent(value))))); | ||
export const deserializeQueryString = (fullQueryString) => { | ||
const queryString = fullQueryString?.replace?.('?', ''); // trim starting ? | ||
if (!queryString) { | ||
return {}; | ||
} | ||
const entries = queryString | ||
.split('&') | ||
.map((value) => value.split('=')) | ||
.filter(([key, value]) => key?.length && value?.length); // filter out empty keys | ||
const dedupedValues = entries | ||
.reduce((prev, current) => { | ||
const currentKey = current[0]; | ||
const currentValue = tryDecodeQueryParam(current[1]); | ||
const existing = prev.find(([key]) => key === currentKey); | ||
if (existing) { | ||
existing[1] instanceof Array ? existing[1].push(currentValue) : (existing[1] = currentValue); | ||
return [...prev]; | ||
} | ||
const newValue = [currentKey, currentKey.includes('[]') ? [currentValue] : currentValue]; | ||
return [...prev, newValue]; | ||
}, []) | ||
.map(([key, value]) => [key.replace('[]', ''), value]); | ||
return Object.fromEntries(dedupedValues); | ||
const params = [...new URLSearchParams(fullQueryString).entries()] | ||
.filter(([key, value]) => key && value) | ||
.map(([key, value]) => [key, decode(value)]); | ||
return Object.fromEntries(params); | ||
}; | ||
//# sourceMappingURL=deserialize-query-string.js.map |
@@ -1,3 +0,13 @@ | ||
export declare const serializeValue: ([key, value]: [key: string, value: any]) => string; | ||
export declare const serializeToQueryString: <T extends object>(query: T) => string; | ||
/** | ||
* Serialize steps: | ||
* 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}' | ||
* 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' | ||
* 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 | ||
* 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0=' | ||
* 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D' | ||
* @param value The value to encode | ||
* @returns The encoded value that can be used as an URL search parameter | ||
*/ | ||
export declare const serializeValue: (value: any) => string; | ||
export declare const serializeToQueryString: <T extends object>(queryObject: T) => string; | ||
//# sourceMappingURL=serialize-to-query-string.d.ts.map |
@@ -1,18 +0,17 @@ | ||
export const serializeValue = ([key, value]) => { | ||
if (typeof value === 'object') { | ||
if (value instanceof Array) { | ||
if (!value.some((v) => typeof v === 'object')) { | ||
return value.map((val) => `${key}[]=${encodeURIComponent(val)}`).join('&'); | ||
} | ||
} | ||
return `${key}=${encodeURIComponent(JSON.stringify(value))}`; | ||
} | ||
return `${key}=${encodeURIComponent(value)}`; | ||
}; | ||
export const serializeToQueryString = (query) => { | ||
return Object.entries(query) | ||
/** | ||
* Serialize steps: | ||
* 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}' | ||
* 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' | ||
* 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 | ||
* 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0=' | ||
* 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D' | ||
* @param value The value to encode | ||
* @returns The encoded value that can be used as an URL search parameter | ||
*/ | ||
export const serializeValue = (value) => encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(value))))); | ||
export const serializeToQueryString = (queryObject) => { | ||
return new URLSearchParams(Object.fromEntries(Object.entries(queryObject) | ||
.filter(([, value]) => value !== undefined) | ||
.map(serializeValue) | ||
.join('&'); | ||
.map(([key, value]) => [key, serializeValue(value)]))).toString(); | ||
}; | ||
//# sourceMappingURL=serialize-to-query-string.js.map |
{ | ||
"name": "@furystack/rest", | ||
"version": "5.0.9", | ||
"version": "5.0.10", | ||
"description": "Generic REST package", | ||
@@ -5,0 +5,0 @@ "type": "module", |
import { deserializeQueryString } from './deserialize-query-string' | ||
import { serializeToQueryString } from './serialize-to-query-string' | ||
import { serializeToQueryString, serializeValue } from './serialize-to-query-string' | ||
import { describe, it, expect } from 'vitest' | ||
describe('deserializeQueryString', () => { | ||
it('Should serialize a null value', () => { | ||
it('Should deserialize a null value', () => { | ||
expect(deserializeQueryString(null as any)).toEqual({}) | ||
}) | ||
it('Should serialize an undefined value', () => { | ||
it('Should deserialize an undefined value', () => { | ||
expect(deserializeQueryString(undefined as any)).toEqual({}) | ||
}) | ||
it('Should serialize an empty string value', () => { | ||
it('Should deserialize an empty string value', () => { | ||
expect(deserializeQueryString('')).toEqual({}) | ||
}) | ||
it('Should serialize a string value with no keys / values', () => { | ||
it('Should deserialize a string value with no keys / values', () => { | ||
expect(deserializeQueryString('?')).toEqual({}) | ||
}) | ||
it('Should serialize a string with given value but empty key', () => { | ||
it('Should deserialize a string with given value but empty key', () => { | ||
expect(deserializeQueryString('?=alma')).toEqual({}) | ||
}) | ||
it('Should serialize a string with given key but empty value', () => { | ||
it('Should deserialize a string with given key but empty value', () => { | ||
expect(deserializeQueryString('?alma=')).toEqual({}) | ||
}) | ||
it('Should serialize a list of primitive values', () => { | ||
expect(deserializeQueryString('?foo=value&bar=2&baz=false')).toEqual({ foo: 'value', bar: 2, baz: false }) | ||
it('Should deserialize a list of primitive values', () => { | ||
expect( | ||
deserializeQueryString(`?foo=${serializeValue('value')}&bar=${serializeValue(2)}&baz=${serializeValue(false)}`), | ||
).toEqual({ | ||
foo: 'value', | ||
bar: 2, | ||
baz: false, | ||
}) | ||
}) | ||
it('Should serialize an array', () => { | ||
expect(deserializeQueryString('?foo[]=value&foo[]=2&foo[]=false')).toEqual({ foo: ['value', 2, false] }) | ||
}) | ||
it('Should override a value if not specified as an array', () => { | ||
expect(deserializeQueryString('?foo=value&foo=2&foo=false&foo=bar')).toEqual({ foo: 'bar' }) | ||
expect( | ||
deserializeQueryString( | ||
`?foo=${serializeValue('value')}&foo=${serializeValue(2)}&foo=${serializeValue(false)}&foo=${serializeValue( | ||
'bar', | ||
)}`, | ||
), | ||
).toEqual({ foo: 'bar' }) | ||
}) | ||
@@ -63,4 +71,4 @@ | ||
it('Should deserialize escaped values', () => { | ||
expect(deserializeQueryString('?alma=asd%2F*-%40')).toEqual({ alma: 'asd/*-@' }) | ||
expect(deserializeQueryString(`?alma=${serializeValue('asd/*-@?')}`)).toEqual({ alma: 'asd/*-@?' }) | ||
}) | ||
}) |
@@ -1,50 +0,15 @@ | ||
export const tryDecodeQueryParam = (queryParam: any) => { | ||
try { | ||
return JSON.parse(decodeURIComponent((queryParam as any).toString())) | ||
} catch { | ||
try { | ||
return JSON.parse(queryParam.toString()) | ||
} catch { | ||
try { | ||
return decodeURIComponent(queryParam.toString()) | ||
} catch { | ||
return queryParam | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* | ||
* Decoding steps: See the encoding steps in reverse order | ||
* @param value The value to decode | ||
* @returns The decoded value | ||
*/ | ||
export const decode = <T>(value: string) => JSON.parse(decodeURIComponent(escape(atob(decodeURIComponent(value))))) as T | ||
export const deserializeQueryString = (fullQueryString: string) => { | ||
const queryString = fullQueryString?.replace?.('?', '') // trim starting ? | ||
const params = [...new URLSearchParams(fullQueryString).entries()] | ||
.filter(([key, value]) => key && value) | ||
.map(([key, value]) => [key, decode(value)]) | ||
if (!queryString) { | ||
return {} | ||
} | ||
const entries = queryString | ||
.split('&') | ||
.map((value) => value.split('=')) | ||
.filter(([key, value]) => key?.length && value?.length) // filter out empty keys | ||
const dedupedValues = entries | ||
.reduce( | ||
(prev, current) => { | ||
const currentKey = current[0] | ||
const currentValue = tryDecodeQueryParam(current[1]) | ||
const existing = prev.find(([key]) => key === currentKey) | ||
if (existing) { | ||
existing[1] instanceof Array ? existing[1].push(currentValue) : (existing[1] = currentValue) | ||
return [...prev] | ||
} | ||
const newValue = [currentKey, currentKey.includes('[]') ? [currentValue] : currentValue] as [ | ||
string, | ||
string | string[], | ||
] | ||
return [...prev, newValue] | ||
}, | ||
[] as Array<[string, string | string[]]>, | ||
) | ||
.map(([key, value]) => [key.replace('[]', ''), value]) | ||
return Object.fromEntries(dedupedValues) | ||
return Object.fromEntries(params) | ||
} |
@@ -6,16 +6,20 @@ import { serializeToQueryString } from './serialize-to-query-string' | ||
it('Should serialize primitive values', () => { | ||
expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: 0, e: null })).toBe('a=1&b=false&c=foo&d=0&e=null') | ||
expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: 0, e: null })).toMatchInlineSnapshot( | ||
'"a=MQ%253D%253D&b=ZmFsc2U%253D&c=ImZvbyI%253D&d=MA%253D%253D&e=bnVsbA%253D%253D"', | ||
) | ||
}) | ||
it('Should exclude explicit undefined', () => { | ||
expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: undefined })).toBe('a=1&b=false&c=foo') | ||
expect(serializeToQueryString({ a: 1, b: false, c: 'foo', d: undefined })).toMatchInlineSnapshot( | ||
'"a=MQ%253D%253D&b=ZmFsc2U%253D&c=ImZvbyI%253D"', | ||
) | ||
}) | ||
it('Should serialize primitive arrays', () => { | ||
expect(serializeToQueryString({ array: [1, 2, 3, 4] })).toBe('array[]=1&array[]=2&array[]=3&array[]=4') | ||
expect(serializeToQueryString({ array: [1, 2, 3, 4] })).toMatchInlineSnapshot('"array=WzEsMiwzLDRd"') | ||
}) | ||
it('Should serialize objects', () => { | ||
expect(serializeToQueryString({ foo: { a: 1, b: 'value' } })).toBe( | ||
`foo=${encodeURIComponent('{"a":1,"b":"value"}')}`, | ||
expect(serializeToQueryString({ foo: { a: 1, b: 'value' } })).toMatchInlineSnapshot( | ||
'"foo=eyJhIjoxLCJiIjoidmFsdWUifQ%253D%253D"', | ||
) | ||
@@ -26,4 +30,4 @@ }) | ||
const array = [1, 2, 3, { foo: 1 }] | ||
expect(serializeToQueryString({ array })).toBe(`array=${encodeURIComponent(JSON.stringify(array))}`) | ||
expect(serializeToQueryString({ array })).toMatchInlineSnapshot('"array=WzEsMiwzLHsiZm9vIjoxfV0%253D"') | ||
}) | ||
}) |
@@ -1,18 +0,22 @@ | ||
export const serializeValue = ([key, value]: [key: string, value: any]) => { | ||
if (typeof value === 'object') { | ||
if (value instanceof Array) { | ||
if (!value.some((v) => typeof v === 'object')) { | ||
return value.map((val) => `${key}[]=${encodeURIComponent(val)}`).join('&') | ||
} | ||
} | ||
return `${key}=${encodeURIComponent(JSON.stringify(value))}` | ||
} | ||
return `${key}=${encodeURIComponent(value)}` | ||
} | ||
/** | ||
* Serialize steps: | ||
* 1. Stringify the value (even primitives, ensure type safety), e.g.: { foo: 'bar😉' } => '{"foo":"bar😉"}' | ||
* 2. Encode as an URI Component, e.g.: ''{"foo":"bar😉"}'' => '%7B%22foo%22%3A%22bar%F0%9F%98%89%22%7D' | ||
* 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 | ||
* 4. Encode the string as base64, e.g.: '{"foo":"barð\x9F\x98\x89"}' => 'eyJmb28iOiJiYXLwn5iJIn0=' | ||
* 5. Encode as an URL Param: 'eyJmb28iOiJiYXLwn5iJIn0=' => 'eyJmb28iOiJiYXLwn5iJIn0%3D' | ||
* @param value The value to encode | ||
* @returns The encoded value that can be used as an URL search parameter | ||
*/ | ||
export const serializeValue = (value: any) => | ||
encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(value))))) | ||
export const serializeToQueryString = <T extends object>(query: T): string => { | ||
return Object.entries(query) | ||
.filter(([, value]) => value !== undefined) | ||
.map(serializeValue) | ||
.join('&') | ||
export const serializeToQueryString = <T extends object>(queryObject: T): string => { | ||
return new URLSearchParams( | ||
Object.fromEntries( | ||
Object.entries(queryObject) | ||
.filter(([, value]) => value !== undefined) | ||
.map(([key, value]) => [key, serializeValue(value)]), | ||
), | ||
).toString() | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45427
377