diff --git a/lib/route-recognizer.ts b/lib/route-recognizer.ts index 0e6b1bf..13c53af 100644 --- a/lib/route-recognizer.ts +++ b/lib/route-recognizer.ts @@ -430,7 +430,7 @@ function recognizeChar( } export interface QueryParams { - [param: string]: string[] | string | null | undefined; + [param: string]: Record | string[] | string | null | undefined; } export interface Result { @@ -688,32 +688,47 @@ class RouteRecognizer { } generateQueryString(params: Params): string { - const pairs: string[] = []; - const keys: string[] = Object.keys(params); - keys.sort(); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = params[key]; - if (value == null) { - continue; + const reducer = (obj: Params, parentPrefix: string | null = null) => (prev: string[], key: string) => { + const val = obj[key]; + if (val === null || val === undefined) { + return prev; } - let pair = encodeURIComponent(key); - if (isArray(value)) { - for (let j = 0; j < value.length; j++) { - const arrayPair = key + "[]" + "=" + encodeURIComponent(value[j]); - pairs.push(arrayPair); - } - } else { - pair += "=" + encodeURIComponent(value as string); - pairs.push(pair); + + const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key; + + if (val == null || typeof val === 'function') { + prev.push(`${prefix}=`); + return prev; } - } + + if (isArray(val)) { + for (let j = 0; j < val.length; j++) { + // handle array query params. array format brackets. (Other options are indices a[0]=b&a[1]=c or repeat a=b&a=c) + if (['string', 'number', 'boolean'].includes(typeof val[j])) { + const arrayPair = key + "[]" + "=" + encodeURIComponent(val[j]); + prev.push(arrayPair); + } else { + prev.push(Object.keys(val[j] as Params).sort().reduce(reducer(val[j] as Params, prefix + `[${j}]`), []).join('&')); + } + } - if (pairs.length === 0) { - return ""; + return prev; + } else if (['string', 'number', 'boolean'].includes(typeof val)) { + prev.push(`${prefix}=${encodeURIComponent(val as string)}`); + return prev; + } + + prev.push(Object.keys(val as Params).sort().reduce(reducer(val as Params, parentPrefix ? prefix : `${prefix}=`), []).join('&')); + return prev; + }; + + const sortedKeys = Object.keys(params).sort(); + // avoid appending unnecessary '?' + if (!sortedKeys.length || sortedKeys.every((k) => params[k] === undefined || params[k] === null)) { + return ''; } - return "?" + pairs.join("&"); + return '?' + sortedKeys.reduce(reducer(params), []).join('&'); } parseQueryString(queryString: string): QueryParams { diff --git a/tests/recognizer-tests.ts b/tests/recognizer-tests.ts index 1490c61..cf748c8 100644 --- a/tests/recognizer-tests.ts +++ b/tests/recognizer-tests.ts @@ -817,6 +817,23 @@ QUnit.test( } ); +QUnit.test("Deserialize query param nested object", (assert: Assert) => { + const handler = {}; + const router = new RouteRecognizer<{}>(); + router.add([{ path: "/foo/bar", handler }]); + + const results = queryParams(router.recognize("/foo/bar?filter=[user][name][$contains]=nick")); + assert.deepEqual(results, { + filter: { + user: { + name: { + $contains: 'nick' + } + } + } + }); +}); + QUnit.test("Multiple `/` routes recognize", (assert: Assert) => { const handler1 = { handler: 1 }; const handler2 = { handler: 2 }; @@ -1629,7 +1646,7 @@ QUnit.module("Route Generation", hooks => { QUnit.test( "Parsing and generation results into the same input string", (assert: Assert) => { - const query = "filter%20data=date"; + const query = "filter data=date"; assert.equal( router.generateQueryString(router.parseQueryString(query)), "?" + query @@ -1721,6 +1738,18 @@ QUnit.module("Route Generation", hooks => { router.generate("index", { queryParams: { filter: "date", sort: 0 } }), "/?filter=date&sort=0" ); + assert.equal( + router.generate("index", { queryParams: { filter: { age: 10, user: { name: { $contains: 'scoot' } } }, sort: 0 } }), + "/?filter=[age]=10&filter=[user][name][$contains]=scoot&sort=0" + ); + assert.equal( + router.generate("index", { queryParams: { sort: 0, filter: { age: 0, user: { name: { $contains: 'scoot' } } } } }), + "/?filter=[age]=0&filter=[user][name][$contains]=scoot&sort=0" + ); + assert.equal( + router.generate("index", { queryParams: { sort: 0, filter: { name: 'bike', children: [{ name: { $contains: 'scoot' } }, { name: { $contains: 'er' }}] } }, sort: 0 }), + "/?filter=[children][0][name][$contains]=scoot&filter=[children][1][name][$contains]=er&filter=[name]=bike&sort=0" + ); }); QUnit.test("Generation works with array query params", (assert: Assert) => {