diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..b85fc2a9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 14 + - 12 + - 10 + - 8 + - 6 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 81f3378f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: - - '14' - - '12' - - '10' - - '8' - - '6' diff --git a/index.d.ts b/index.d.ts index 026d287d..b6d651b2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -409,3 +409,81 @@ export function stringifyUrl( object: UrlObject, options?: StringifyOptions ): string; + +/** +Pick query parameters from a URL. + +@param url - The URL containing the query parameters to pick. +@param keys - The names of the query parameters to keep. All other query parameters will be removed from the URL. +@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +@returns The URL with the picked query parameters. + +@example +``` +queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?foo=1#hello' + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?bar=2#hello' +``` +*/ +export function pick( + url: string, + keys: readonly string[], + options?: ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | boolean | number) => boolean, + options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | boolean) => boolean, + options?: {parseBooleans: true} & ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | number) => boolean, + options?: {parseNumbers: true} & ParseOptions & StringifyOptions +): string + +/** +Exclude query parameters from a URL. Like `.pick()` but reversed. + +@param url - The URL containing the query parameters to exclude. +@param keys - The names of the query parameters to remove. All other query parameters will remain in the URL. +@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +@returns The URL without the excluded the query parameters. + +@example +``` +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?bar=2#hello' + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?foo=1#hello' +``` +*/ +export function exclude( + url: string, + keys: readonly string[], + options?: ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | boolean | number) => boolean, + options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | boolean) => boolean, + options?: {parseBooleans: true} & ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | number) => boolean, + options?: {parseNumbers: true} & ParseOptions & StringifyOptions +): string diff --git a/index.js b/index.js index 559ecd47..423b9d6b 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const strictUriEncode = require('strict-uri-encode'); const decodeComponent = require('decode-uri-component'); const splitOnFirst = require('split-on-first'); +const filterObject = require('filter-obj'); const isNullOrUndefined = value => value === null || value === undefined; @@ -244,6 +245,10 @@ function parse(query, options) { } for (const param of query.split('&')) { + if (param === '') { + continue; + } + let [key, value] = splitOnFirst(options.decode ? param.replace(/\+/g, ' ') : param, '='); // Missing `=` should be `null`: @@ -378,3 +383,22 @@ exports.stringifyUrl = (object, options) => { return `${url}${queryString}${hash}`; }; + +exports.pick = (input, filter, options) => { + options = Object.assign({ + parseFragmentIdentifier: true + }, options); + + const {url, query, fragmentIdentifier} = exports.parseUrl(input, options); + return exports.stringifyUrl({ + url, + query: filterObject(query, filter), + fragmentIdentifier + }, options); +}; + +exports.exclude = (input, filter, options) => { + const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value); + + return exports.pick(input, exclusionFilter, options); +}; diff --git a/index.test-d.ts b/index.test-d.ts index 2aab3fe1..20325841 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -124,3 +124,9 @@ expectType( }, }) ); + +// Pick +expectType(queryString.pick('http://foo.bar/?abc=def&hij=klm', ['abc'])) + +// Exclude +expectType(queryString.exclude('http://foo.bar/?abc=def&hij=klm', ['abc'])) diff --git a/package.json b/package.json index df8a1f25..3b90b263 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "query-string", - "version": "6.13.8", + "version": "6.14.0", "description": "Parse and stringify URL query strings", "license": "MIT", "repository": "sindresorhus/query-string", @@ -34,10 +34,12 @@ "stringify", "encode", "decode", - "searchparams" + "searchparams", + "filter" ], "dependencies": { "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" }, diff --git a/readme.md b/readme.md index 46e58b5b..280972e3 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# query-string [![Build Status](https://travis-ci.com/sindresorhus/query-string.svg?branch=master)](https://travis-ci.com/github/sindresorhus/query-string) [![](https://badgen.net/bundlephobia/minzip/query-string)](https://bundlephobia.com/result?p=query-string) +# query-string > Parse and stringify URL [query strings](https://en.wikipedia.org/wiki/Query_string) @@ -16,7 +16,7 @@ Special thanks to:

- +

@@ -415,6 +415,64 @@ Type: `object` Query items to add to the URL. +### .pick(url, keys, options?) +### .pick(url, filter, options?) + +Pick query parameters from a URL. + +Returns a string with the new URL. + +```js +const queryString = require('query-string'); + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?foo=1#hello' + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?bar=2#hello' +``` + +### .exclude(url, keys, options?) +### .exclude(url, filter, options?) + +Exclude query parameters from a URL. + +Returns a string with the new URL. + +```js +const queryString = require('query-string'); + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?bar=2#hello' + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?foo=1#hello' +``` + +#### url + +Type: `string` + +The URL containing the query parameters to filter. + +#### keys + +Type: `string[]` + +The names of the query parameters to filter based on the function used. + +#### filter + +Type: `(key, value) => boolean` + +A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +#### options + +Type: `object` + +[Parse options](#options) and [stringify options](#options-1). + ## Nesting This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of [edge cases](https://github.com/visionmedia/node-querystring/issues). diff --git a/test/exclude.js b/test/exclude.js new file mode 100644 index 00000000..91e0d4f5 --- /dev/null +++ b/test/exclude.js @@ -0,0 +1,17 @@ +import test from 'ava'; +import queryString from '..'; + +test('excludes elements in a URL with a filter array', t => { + t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', ['c']), 'http://example.com/?a=1&b=2#a'); +}); + +test('excludes elements in a URL with a filter predicate', t => { + t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', (name, value) => { + t.is(typeof name, 'string'); + t.is(typeof value, 'number'); + + return name === 'a'; + }, { + parseNumbers: true + }), 'http://example.com/?b=2&c=3#a'); +}); diff --git a/test/parse.js b/test/parse.js index 365eb2f7..9bfc0344 100644 --- a/test/parse.js +++ b/test/parse.js @@ -13,6 +13,11 @@ test('query strings starting with a `&`', t => { t.deepEqual(queryString.parse('&foo=bar&foo=baz'), {foo: ['bar', 'baz']}); }); +test('query strings ending with a `&`', t => { + t.deepEqual(queryString.parse('foo=bar&'), {foo: 'bar'}); + t.deepEqual(queryString.parse('foo=bar&&&'), {foo: 'bar'}); +}); + test('parse a query string', t => { t.deepEqual(queryString.parse('foo=bar'), {foo: 'bar'}); }); diff --git a/test/pick.js b/test/pick.js new file mode 100644 index 00000000..e5e43812 --- /dev/null +++ b/test/pick.js @@ -0,0 +1,17 @@ +import test from 'ava'; +import queryString from '..'; + +test('picks elements in a URL with a filter array', t => { + t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', ['a', 'b']), 'http://example.com/?a=1&b=2#a'); +}); + +test('picks elements in a URL with a filter predicate', t => { + t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', (name, value) => { + t.is(typeof name, 'string'); + t.is(typeof value, 'number'); + + return name === 'a'; + }, { + parseNumbers: true + }), 'http://example.com/?a=1#a'); +});