├── .github └── workflows │ ├── CODEOWNERS │ ├── ci.yaml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE.txt ├── README.md ├── deno.json ├── deno.lock ├── getters ├── getMultipleQueryParams.test.ts ├── getMultipleQueryParams.ts ├── getMultipleQueryParamsCurried.test.ts ├── getMultipleQueryParamsCurried.ts ├── getSingleQueryParam.test.ts ├── getSingleQueryParam.ts ├── getSingleQueryParamCurried.test.ts ├── getSingleQueryParamCurried.ts └── index.ts ├── index.ts ├── mutators ├── _internal │ ├── queryMutation.test.ts │ └── queryMutation.ts ├── filterQueryParams.test.ts ├── filterQueryParams.ts ├── index.ts ├── pushQueryParams.test.ts ├── pushQueryParams.ts ├── removeQueryParams.test.ts ├── removeQueryParams.ts ├── resetQuery.test.ts ├── resetQuery.ts ├── setQueryParams.test.ts └── setQueryParams.ts ├── predicates ├── index.ts ├── isQueryEmpty.test.ts └── isQueryEmpty.ts ├── test └── index.ts └── types └── ParsedUrlQuery.ts /.github/workflows/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @honey32 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI (using deno) 2 | 3 | on: 4 | push: 5 | branches: [ v2 ] 6 | pull_request: 7 | branches: [ v2 ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: denoland/setup-deno@v1 15 | with: 16 | deno-version: v1.x # Run with latest stable Deno 17 | 18 | - name: Check if already formatted 19 | run: deno fmt --check 20 | 21 | - name: Lint 22 | run: deno lint 23 | 24 | - name: Test 25 | run: deno test -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish: 8 | if: startsWith(github.ref, 'refs/tags/v2.') 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Publish package 19 | run: npx jsr publish 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for previous version's outputs and generated files (npm / percel / typedoc) 2 | dist-doc 3 | .parcel-cache 4 | dist 5 | node_modules -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "denoland.vscode-deno", 3 | "editor.formatOnSave": true, 4 | "[json]": { 5 | "editor.defaultFormatter": "denoland.vscode-deno" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "denoland.vscode-deno" 9 | }, 10 | "prettier.enable": false, 11 | "eslint.enable": false, 12 | "biome.enabled": false 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 honey32 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Query Utils 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | > [!NOTE] 6 | > 7 | > This lib no longer updates _in npm_. latest: 8 | > [![npm version](https://badge.fury.io/js/next-query-utils.svg)](https://badge.fury.io/js/next-query-utils) 9 | > 10 | > Instead, use 11 | > [JSR where it's published as v2.x or later](https://jsr.io/@honey32/next-query-utils) 12 | 13 | This library provides utility functions to deal with **Parsed Query Objects** 14 | (especially of Next.js) 15 | 16 | このライブラリには、**Parsed Query Object** (特に Next.js 17 | のもの)を取り扱うためのユーティリティ関数群が含まれます。 18 | 19 | ## Migration from v1 to v2 20 | 21 | As the only difference; v2 is provided as 22 | [`@honey32/next-query-utils` via JSR](https://jsr.io/@honey32/next-query-utils), 23 | while v1 as `next-query-utils` via npm. 24 | 25 | When you try to migrate from npm to JSR, see: 26 | https://jsr.io/docs/npm-compatibility 27 | 28 | # Usages — 使い方 29 | 30 | ## Getting single value — 単独の値を取得する 31 | 32 | `?id=aaa` or `?id=aaa&id=other` -> `"aaa"` 33 | 34 | ```ts 35 | // before 36 | const _id = router.query["id"]; 37 | const id = Array.isArray(id) ? id[0] : id; 38 | 39 | // after 40 | const id = getSingleQueryParam(router.query, "id"); 41 | ``` 42 | 43 | ## Removing some params — 値を取り除く 44 | 45 | `?start_on=2022-03-02&item_type=stationary&item_type=book` -> 46 | `?start_on=2022-03-02&item_type=stationary` 47 | 48 | ### Before 49 | 50 |
Code (I don't want to write such an annoying code any more.)
二度と書きたくないひどいコード
51 | 52 | ```ts 53 | // before 54 | const removeQuery = ( 55 | query: ParsedUrlQuery, 56 | key: string, 57 | pred: string, 58 | ) => { 59 | const value = query[key]; 60 | 61 | // if empty, leave query as it is. 62 | if (!value) return query; 63 | if (Array.isArray(value)) { 64 | if (value.length === 0) return query; 65 | 66 | // if non-empty array of string 67 | return { ...acc, [key]: value.filter((s) => s !== pred) }; 68 | } 69 | 70 | // if single string (not empty) 71 | return { ...acc, [key]: (s !== value) ? value : [] }; 72 | }; 73 | ``` 74 | 75 |
76 |
77 | 78 | ### After 79 | 80 | ```ts 81 | // after 82 | router.push( 83 | removeQueryParam({ 84 | item_type: "book", 85 | })(router.query), 86 | ); 87 | ``` 88 | 89 | ## Keeping some params (or Next.js's dynamic routes) from being reset
— (Next.js's の動的ルートや)パラメータを残して他を削除する 90 | 91 | `/[postId]?other=value&other2=value` -> `/[postId]` 92 | 93 | --- 94 | 95 | In pages with 96 | [Next.js's dynamic routes](https://nextjs.org/docs/routing/dynamic-routes), 97 | `router.query` include them (in this example, `.postId`). so they **MUST be kept 98 | from resetting**. 99 | 100 | In this case, use `resetQuery()` with `ignore` option. 101 | 102 | --- 103 | 104 | [Next.js の動的ルート](https://nextjs.org/docs/routing/dynamic-routes)があるページでは、それが 105 | `router.query` に含まれる。(この例では `.postId`) なので、それらは 106 | **削除してはいけない**。 107 | 108 | このようなケースでは `resetQuery()` と `ignore` オプションを使いましょう。 109 | 110 | --- 111 | 112 | ```ts 113 | // before 114 | router.push({ postId: router.query["postId"] }); 115 | 116 | // after 117 | router.push(resetQuery({ ignore: "postId" })(router.query)); 118 | ``` 119 | 120 | ## Checking if query is empty ignoring some params (e.g. dynamic routes)
— (動的ルートのような)パラメータ幾つかを無視して、クエリが空であるか確かめる 121 | 122 | - _True_ if `/items/[postId]` 123 | - _False_ if `/items/[postId]?param1=aa` 124 | 125 | --- 126 | 127 | Likewise, you need to ignore _dynamic routes_ in order to check if the query is 128 | empty. 129 | 130 | In this case, use `isQueryEmpty()` with `ignore` option. 131 | 132 | --- 133 | 134 | 前の例と同じように、クエリが空であるか確かめるためには、 _動的ルート_ 135 | を無視する必要があります。 136 | 137 | このようなケースでは、`isQueryEmpty()` と `ignore` オプションを使いましょう。 138 | 139 | --- 140 | 141 | ```ts 142 | isQueryEmpty(router.query, { ignore: "postId" }); 143 | ``` 144 | 145 | # License 146 | 147 | This library is licensed under the terms of [MIT License](/license) 148 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@honey32/next-query-utils", 3 | "version": "2.0.0", 4 | "exports": "./index.ts", 5 | "imports": { 6 | "@std/assert": "jsr:@std/assert@^1.0.0", 7 | "@std/testing": "jsr:@std/testing@^0.225.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "jsr:@std/assert@1.0.0-rc.2": "jsr:@std/assert@1.0.0-rc.2", 6 | "jsr:@std/assert@^1.0.0": "jsr:@std/assert@1.0.0", 7 | "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1", 8 | "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1", 9 | "jsr:@std/testing@^0.225.3": "jsr:@std/testing@0.225.3" 10 | }, 11 | "jsr": { 12 | "@std/assert@1.0.0": { 13 | "integrity": "0e4f6d873f7f35e2a1e6194ceee39686c996b9e5d134948e644d35d4c4df2008", 14 | "dependencies": [ 15 | "jsr:@std/internal@^1.0.1" 16 | ] 17 | }, 18 | "@std/assert@1.0.0-rc.2": { 19 | "integrity": "0484eab1d76b55fca1c3beaff485a274e67dd3b9f065edcbe70030dfc0b964d3", 20 | "dependencies": [ 21 | "jsr:@std/internal@^1.0.0" 22 | ] 23 | }, 24 | "@std/internal@1.0.1": { 25 | "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" 26 | }, 27 | "@std/testing@0.225.3": { 28 | "integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780", 29 | "dependencies": [ 30 | "jsr:@std/assert@1.0.0-rc.2" 31 | ] 32 | } 33 | } 34 | }, 35 | "remote": {}, 36 | "workspace": { 37 | "dependencies": [ 38 | "jsr:@std/assert@^1.0.0", 39 | "jsr:@std/testing@^0.225.3" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /getters/getMultipleQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { getMultipleQueryParams } from "./getMultipleQueryParams.ts"; 4 | 5 | describe("getMultipleQueryParams(key, pred)(query)", () => { 6 | type Case = [query: ParsedUrlQuery, result: string[]]; 7 | 8 | ([ 9 | [{ key: "a" }, ["a"]], 10 | [{ key: [] }, []], 11 | [{}, /* */ []], 12 | ] satisfies Case[]).forEach(([query, result]) => { 13 | it(`(${JSON.stringify(query)}, "key") === ${JSON.stringify(result)}`, () => { 14 | assertEquals(getMultipleQueryParams(query, "key"), result); 15 | }); 16 | }); 17 | 18 | ([ 19 | [{ key: "a" }, ["a"]], 20 | [{ key: ["a"] }, ["a"]], 21 | [{ key: ["a", "b"] }, ["a"]], 22 | [{ key: ["b", "a"] }, ["a"]], 23 | [{}, /* */ []], 24 | [{ key: "b" }, []], 25 | [{ key: ["b"] }, []], 26 | [{ key: ["b", "c"] }, []], 27 | ] satisfies Case[]).forEach(([query, result]) => { 28 | it(`(${JSON.stringify(query)}, "key", (s) => s === "a") === ${JSON.stringify(result)}`, () => { 29 | assertEquals( 30 | getMultipleQueryParams(query, "key", (s) => s === "a"), 31 | result, 32 | ); 33 | 34 | // strictly typed 35 | assertEquals( 36 | getMultipleQueryParams( 37 | query, 38 | "key", 39 | (s): s is "a" => s === "a", 40 | ) satisfies "a"[], 41 | result, 42 | ); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /getters/getMultipleQueryParams.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 2 | 3 | /** 4 | * Returns an array of the values for the specified *key* in the query object. 5 | * If `pred` specified, returns the values *that meet it*. 6 | * 7 | * --- 8 | * 9 | * 返り値は、指定した `key` に対応するパラメータの値の配列。 10 | * もし `pred` が指定されている場合は、その関数によって返り値の配列が filter される。 11 | * 12 | * @example 13 | * ``` 14 | * getMultipleQueryParams({}, "id") === [] 15 | * getMultipleQueryParams({ id: "aaa" }, "id") === ["aaa"] 16 | * 17 | * // with pred specified 18 | * const is_a = (s: string) => s === "a" 19 | * getMultipleQueryParams({ id: "a" }, "id", is_a) === ["a"] 20 | * getMultipleQueryParams({}, "id", is_a) === [] 21 | * getMultipleQueryParams({ id: "b" }, "id", is_a) === [] 22 | * getMultipleQueryParams({ id: ["b", "a"] }, "id", is_a) === ["a"] 23 | * ``` 24 | * 25 | * @param pred *optional*. the values *that fit this predicate* will be returned. 26 | */ 27 | export function getMultipleQueryParams( 28 | query: ParsedUrlQuery, 29 | key: string, 30 | pred?: (s: string) => s is T, 31 | ): T[]; 32 | 33 | /** 34 | * Returns an array of the values for the specified *key* in the query object. 35 | * If `pred` specified, returns the values *that meet it*. 36 | * 37 | * --- 38 | * 39 | * 返り値は、指定した `key` に対応するパラメータの値の配列。 40 | * もし `pred` が指定されている場合は、その関数によって返り値の配列が filter される。 41 | * 42 | * @example 43 | * ``` 44 | * getMultipleQueryParams({}, "id") === [] 45 | * getMultipleQueryParams({ id: "aaa" }, "id") === ["aaa"] 46 | * 47 | * // with pred specified 48 | * const is_a = (s: string) => s === "a" 49 | * getMultipleQueryParams({ id: "a" }, "id", is_a) === ["a"] 50 | * getMultipleQueryParams({}, "id", is_a) === [] 51 | * getMultipleQueryParams({ id: "b" }, "id", is_a) === [] 52 | * getMultipleQueryParams({ id: ["b", "a"] }, "id", is_a) === ["a"] 53 | * ``` 54 | * 55 | * @param pred *optional*. the values *that fit this predicate* will be returned. 56 | */ 57 | export function getMultipleQueryParams( 58 | query: ParsedUrlQuery, 59 | key: string, 60 | pred?: (s: string) => boolean, 61 | ): string[]; 62 | 63 | export function getMultipleQueryParams( 64 | query: ParsedUrlQuery, 65 | key: string, 66 | pred: (s: string) => boolean = () => true, 67 | ): string[] { 68 | const _value = query[key]; 69 | 70 | if (!_value) return []; 71 | 72 | if (Array.isArray(_value)) return _value.filter(pred); 73 | 74 | return pred(_value) ? [_value] : []; 75 | } 76 | -------------------------------------------------------------------------------- /getters/getMultipleQueryParamsCurried.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { describe, it } from "../test/index.ts"; 3 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 4 | import { getMultipleQueryParams } from "./getMultipleQueryParams.ts"; 5 | import { getMultipleQueryParamsCurried } from "./getMultipleQueryParamsCurried.ts"; 6 | 7 | describe("getMultipleQueryParamsCurried(key, pred)(query)", () => { 8 | type Case = [query: ParsedUrlQuery]; 9 | 10 | ([ 11 | [{ key: "a" }], // 12 | [{ key: [] }], 13 | [{}], 14 | ] satisfies Case[]).forEach(([query]) => { 15 | it(`("key")(${JSON.stringify(query)}) matches non-curreid version's result`, () => { 16 | const getKeys = getMultipleQueryParamsCurried("key"); 17 | assertEquals(getKeys(query), getMultipleQueryParams(query, "key")); 18 | }); 19 | }); 20 | 21 | ([ 22 | [{ key: "a" }], 23 | [{ key: ["a"] }], 24 | [{ key: ["a", "b"] }], 25 | [{ key: ["b", "a"] }], 26 | [{}], 27 | [{ key: "b" }], 28 | [{ key: ["b"] }], 29 | [{ key: ["b", "c"] }], 30 | ] satisfies Case[]).forEach(([query]) => { 31 | it(`("key", (s) => s === "a")(${JSON.stringify(query)}) matches non-curreid version's result`, () => { 32 | const result = getMultipleQueryParams(query, "key", (s) => s === "a"); 33 | const getKeysLoose = getMultipleQueryParamsCurried( 34 | "key", 35 | (s) => s === "a", 36 | ); 37 | assertEquals(getKeysLoose(query), result); 38 | 39 | // strictly typed 40 | const getKeysStrict = getMultipleQueryParamsCurried( 41 | "key", 42 | (s): s is "a" => s === "a", 43 | ); 44 | assertEquals(getKeysStrict(query) satisfies "a"[], result); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /getters/getMultipleQueryParamsCurried.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 2 | import { getMultipleQueryParams } from "./getMultipleQueryParams.ts"; 3 | 4 | /** 5 | * "data-last" Curried version of {@link getMultipleQueryParams}. 6 | * 7 | * @example 8 | * ``` 9 | * type Char = ...; 10 | * const isChar = (input: string): input is Char => input.length === 0; 11 | * const getKeys = getMultipleQueryParamsCurried("key", isChar) 12 | * ``` 13 | */ 14 | export function getMultipleQueryParamsCurried( 15 | key: string, 16 | pred: (s: string) => s is T, 17 | ): (query: ParsedUrlQuery) => T[]; 18 | 19 | /** 20 | * "data-last" Curried version of {@link getMultipleQueryParams}. 21 | * 22 | * @example 23 | * ``` 24 | * type Char = ...; 25 | * const isChar = (input: string): input is Char => input.length === 0; 26 | * const getKeys = getMultipleQueryParamsCurried("key", isChar) 27 | * ``` 28 | */ 29 | export function getMultipleQueryParamsCurried( 30 | key: string, 31 | pred?: (s: string) => boolean, 32 | ): (query: ParsedUrlQuery) => string[]; 33 | 34 | export function getMultipleQueryParamsCurried( 35 | key: string, 36 | pred?: (s: string) => boolean, 37 | ): (query: ParsedUrlQuery) => string[] { 38 | return (query) => getMultipleQueryParams(query, key, pred); 39 | } 40 | -------------------------------------------------------------------------------- /getters/getSingleQueryParam.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { getSingleQueryParam } from "./getSingleQueryParam.ts"; 4 | 5 | describe("getSingleQueryParam(key, pred)(query)", () => { 6 | type Case = [query: ParsedUrlQuery, result: string | undefined]; 7 | 8 | ([ 9 | [{ key: "a" }, "a"], 10 | [{ key: [] }, undefined], 11 | [{}, undefined], 12 | ] satisfies Case[]).forEach(([query, result]) => { 13 | it(`(${JSON.stringify(query)}, "key") === ${JSON.stringify(result)}`, () => { 14 | assertEquals(getSingleQueryParam(query, "key"), result); 15 | }); 16 | }); 17 | 18 | ([ 19 | [{ key: "a" }, "a"], 20 | [{ key: ["a"] }, "a"], 21 | [{ key: ["a", "b"] }, "a"], 22 | [{ key: ["b", "a"] }, "a"], 23 | [{}, undefined], 24 | [{ key: "b" }, undefined], 25 | [{ key: ["b"] }, undefined], 26 | [{ key: ["b", "c"] }, undefined], 27 | ] satisfies Case[]).forEach(([query, result]) => { 28 | it(`(${JSON.stringify(query)}, "key", (s) => s === "a") === ${JSON.stringify(result)}`, () => { 29 | assertEquals(getSingleQueryParam(query, "key", (s) => s === "a"), result); 30 | 31 | // strictly typed 32 | assertEquals( 33 | getSingleQueryParam(query, "key", (s): s is "a" => s === "a") satisfies 34 | | "a" 35 | | undefined, 36 | result, 37 | ); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /getters/getSingleQueryParam.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 2 | 3 | /** 4 | * Returns the first value for the specified *key* in the query object. 5 | * If `pred` specified, returns the first value *that meets it*. 6 | * 7 | * --- 8 | * 9 | * 返り値は、指定した `key` に対応するパラメータのうち最初の値。 10 | * もし `pred` が指定されている場合は、**その関数に合格した**値のうち最初のものが返る。 11 | * 12 | * @example 13 | * ``` 14 | * getSingleQueryParam({}, "id") === undefined 15 | * getSingleQueryParam({ id: "aaa" }, "id") === "aaa" 16 | * 17 | * // with pred specified 18 | * const is_a = (s: string) => s === "a" 19 | * getSingleQueryParam({ id: "a" }, "id", is_a) === "a" 20 | * getSingleQueryParam({}, "id", is_a) === undefined 21 | * getSingleQueryParam({ id: "b" }, "id", is_a) === undefined 22 | * getSingleQueryParam({ id: ["b", "a"] }, "id", is_a) === "a" 23 | * ``` 24 | * 25 | * @param pred *optional*. the first value *that fits this predicate* will be returned. 26 | */ 27 | export function getSingleQueryParam( 28 | query: ParsedUrlQuery, 29 | key: string, 30 | pred: (s: string) => s is T, 31 | ): T | undefined; 32 | 33 | /** 34 | * Returns the first value for the specified *key* in the query object. 35 | * If `pred` specified, returns the first value *that meets it*. 36 | * 37 | * --- 38 | * 39 | * 返り値は、指定した `key` に対応するパラメータのうち最初の値。 40 | * もし `pred` が指定されている場合は、**その関数に合格した**値のうち最初のものが返る。 41 | * 42 | * @example 43 | * ``` 44 | * getSingleQueryParam({}, "id") === undefined 45 | * getSingleQueryParam({ id: "aaa" }, "id") === "aaa" 46 | * 47 | * // with pred specified 48 | * const is_a = (s: string) => s === "a" 49 | * getSingleQueryParam({ id: "a" }, "id", is_a) === "a" 50 | * getSingleQueryParam({}, "id", is_a) === undefined 51 | * getSingleQueryParam({ id: "b" }, "id", is_a) === undefined 52 | * getSingleQueryParam({ id: ["b", "a"] }, "id", is_a) === "a" 53 | * ``` 54 | * 55 | * @param pred *optional*. the first value *that fits this predicate* will be returned. 56 | */ 57 | export function getSingleQueryParam( 58 | query: ParsedUrlQuery, 59 | key: string, 60 | pred?: (s: string) => boolean, 61 | ): string | undefined; 62 | 63 | export function getSingleQueryParam( 64 | query: ParsedUrlQuery, 65 | key: string, 66 | pred: (s: string) => boolean = () => true, 67 | ): string | undefined { 68 | const _value = query[key]; 69 | 70 | if (!_value) return undefined; 71 | 72 | if (Array.isArray(_value)) return _value.filter(pred)[0]; 73 | 74 | return pred(_value) ? _value : undefined; 75 | } 76 | -------------------------------------------------------------------------------- /getters/getSingleQueryParamCurried.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { getSingleQueryParam } from "./getSingleQueryParam.ts"; 4 | import { getSingleQueryParamCurried } from "./getSingleQueryParamCurried.ts"; 5 | 6 | describe("getSingleQueryParamCurried(key, pred)(query)", () => { 7 | type Case = [query: ParsedUrlQuery]; 8 | 9 | ([ 10 | [{ key: "a" }], // 11 | [{ key: [] }], 12 | [{}], 13 | ] satisfies Case[]).forEach(([query]) => { 14 | it(`(${JSON.stringify(query)}, "key")`, () => { 15 | const getKey = getSingleQueryParamCurried("key"); 16 | assertEquals(getKey(query), getSingleQueryParam(query, "key")); 17 | }); 18 | }); 19 | 20 | ([ 21 | [{ key: "a" }], 22 | [{ key: ["a"] }], 23 | [{ key: ["a", "b"] }], 24 | [{ key: ["b", "a"] }], 25 | [{}], 26 | [{ key: "b" }], 27 | [{ key: ["b"] }], 28 | [{ key: ["b", "c"] }], 29 | ] satisfies Case[]).forEach(([query]) => { 30 | it(`("key", (s) => s === "a")(${JSON.stringify(query)}) matches non-curried version's result`, () => { 31 | const result = getSingleQueryParam(query, "key", (s) => s === "a"); 32 | const getKeyLoose = getSingleQueryParamCurried("key", (s) => s === "a"); 33 | assertEquals(getKeyLoose(query), result); 34 | 35 | // strictly typed 36 | const isA = (s: string): s is "a" => s === "a"; 37 | const getKeyStrict = getSingleQueryParamCurried("key", isA); 38 | assertEquals(getKeyStrict(query) satisfies "a" | undefined, result); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /getters/getSingleQueryParamCurried.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 2 | import { getSingleQueryParam } from "./getSingleQueryParam.ts"; 3 | 4 | /** 5 | * "data-last" Curried version of {@link getSingleQueryParam}. 6 | * 7 | * @example 8 | * ``` 9 | * const isValidSortOrder = (input: string): input is "asc" | "desc" => input === "asc" || input === "desc" 10 | * const getSingleSortOrder = getSingleQueryParamCurreid("order", isValidSortOrder) 11 | * ``` 12 | */ 13 | export function getSingleQueryParamCurried( 14 | key: string, 15 | pred: (s: string) => s is T, 16 | ): (query: ParsedUrlQuery) => T | undefined; 17 | 18 | /** 19 | * "data-last" Curried version of {@link getSingleQueryParam}. 20 | * 21 | * @example 22 | * ``` 23 | * const isValidSortOrder = (input: string): input is "asc" | "desc" => input === "asc" || input === "desc" 24 | * const getSingleSortOrder = getSingleQueryParamCurreid("order", isValidSortOrder) 25 | * ``` 26 | */ 27 | export function getSingleQueryParamCurried( 28 | key: string, 29 | pred?: (s: string) => boolean, 30 | ): (query: ParsedUrlQuery) => string | undefined; 31 | 32 | export function getSingleQueryParamCurried( 33 | key: string, 34 | pred?: (s: string) => boolean, 35 | ): (query: ParsedUrlQuery) => string | undefined { 36 | return (query) => getSingleQueryParam(query, key, pred); 37 | } 38 | -------------------------------------------------------------------------------- /getters/index.ts: -------------------------------------------------------------------------------- 1 | export { getMultipleQueryParams } from "./getMultipleQueryParams.ts"; 2 | export { getMultipleQueryParamsCurried } from "./getMultipleQueryParamsCurried.ts"; 3 | export { getSingleQueryParam } from "./getSingleQueryParam.ts"; 4 | export { getSingleQueryParamCurried } from "./getSingleQueryParamCurried.ts"; 5 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getters/index.ts"; 2 | export * from "./mutators/index.ts"; 3 | export * from "./predicates/index.ts"; 4 | -------------------------------------------------------------------------------- /mutators/_internal/queryMutation.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../../types/ParsedUrlQuery.ts"; 3 | import { queryMutation } from "./queryMutation.ts"; 4 | 5 | describe("queryMutation()", () => { 6 | type Case = [ 7 | f0: (query: ParsedUrlQuery) => ParsedUrlQuery, 8 | f1: (query: ParsedUrlQuery) => ParsedUrlQuery, 9 | base: ParsedUrlQuery, 10 | result: ParsedUrlQuery, 11 | ]; 12 | 13 | ([ 14 | [ 15 | (q) => ({ ...q, a: "aaa" }), 16 | (q) => ({ ...q, b: "bbb" }), 17 | {}, 18 | { a: "aaa", b: "bbb" }, 19 | ], 20 | [ 21 | ({ a: _, ...rest }) => rest, 22 | ({ b: _, ...rest }) => rest, 23 | { a: "aaa", b: "bbb" }, 24 | {}, 25 | ], 26 | ] satisfies Case[]).forEach(([f0, f1, base, result]) => { 27 | it(`(f0).andThen(f2)(${JSON.stringify(base)}) === ${JSON.stringify(result)}`, () => { 28 | assertEquals(queryMutation(f0).andThen(f1)(base), result); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /mutators/_internal/queryMutation.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../../types/ParsedUrlQuery.ts"; 2 | 3 | export type QueryMutation = ((query: ParsedUrlQuery) => ParsedUrlQuery) & { 4 | andThen(next: (query: ParsedUrlQuery) => ParsedUrlQuery): QueryMutation; 5 | }; 6 | 7 | /** 8 | * Composes [[`QueryMutation`]] to enable function composition. 9 | * 10 | * --- 11 | * 12 | * 関数合成を可能にするため、[[`QueryMutation`]] を作成する。 13 | */ 14 | export const queryMutation = ( 15 | fn: (query: ParsedUrlQuery) => ParsedUrlQuery, 16 | ): QueryMutation => { 17 | type AndThen = Pick; 18 | 19 | return Object.assign(fn, { 20 | andThen: (next) => queryMutation((query) => next(fn(query))), 21 | } as AndThen); 22 | }; 23 | -------------------------------------------------------------------------------- /mutators/filterQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | 3 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 4 | import { filterQueryParams } from "./filterQueryParams.ts"; 5 | 6 | describe("filterQueryParams(options)(query)", () => { 7 | type Case = [ 8 | options: { limit?: number } | undefined, 9 | query: ParsedUrlQuery, 10 | result: ParsedUrlQuery, 11 | ]; 12 | 13 | ([ 14 | [undefined, {}, /* */ {}], 15 | [undefined, { a: "aaa" }, /* */ { a: ["aaa"] }], 16 | [undefined, { a: ["aaa", "aab"] }, { a: ["aaa", "aab"] }], 17 | [{ limit: 4 }, { a: ["aaa", "aab"] }, { a: ["aaa", "aab"] }], 18 | [{ limit: 1 }, { a: ["aaa", "aab"] }, { a: ["aaa"] }], 19 | ] satisfies Case[]).forEach(([options, query, result]) => { 20 | const _options = JSON.stringify(options); 21 | const _query = JSON.stringify(query); 22 | const _result = JSON.stringify(result); 23 | it( 24 | `('a', () => true, ${_options})(${_query}) === ${_result}`, 25 | () => { 26 | assertEquals( 27 | filterQueryParams("a", () => true, options)(query), 28 | result, 29 | ); 30 | }, 31 | ); 32 | }); 33 | 34 | ([ 35 | [undefined, { a: ["aaa", "axa"] }, { a: ["aaa", "axa"] }], 36 | [undefined, { a: ["aaa", "aab"] }, { a: ["aaa"] }], 37 | [{ limit: 1 }, { a: ["aab", "axa", "aaa"] }, { a: ["axa"] }], 38 | ] satisfies Case[]).forEach(([options, query, result]) => { 39 | const _options = JSON.stringify(options); 40 | const _query = JSON.stringify(query); 41 | const _result = JSON.stringify(result); 42 | it( 43 | `('a', (s) => s.endsWith('a'), ${_options})(${_query}) === ${_result}`, 44 | () => { 45 | assertEquals( 46 | filterQueryParams("a", (s) => s.endsWith("a"), options)(query), 47 | result, 48 | ); 49 | }, 50 | ); 51 | }); 52 | 53 | it("testing the whole feature", () => { 54 | const query = { 55 | a: ["axx", "axa", "abb"], 56 | b: "bbb", 57 | }; 58 | const result = { 59 | a: ["axa"], 60 | b: "bbb", 61 | }; 62 | assertEquals( 63 | filterQueryParams("a", (s) => s.endsWith("a"), { limit: 1 })(query), 64 | result, 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /mutators/filterQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | /** 7 | * Filters query parameters for the given `key` using the given `pred`. 8 | * If `limit` option is specified, the excess values will be ommited. 9 | * 10 | * Other parameters remain as they were. 11 | * 12 | * --- 13 | * 14 | * 指定した `key` に対応するパラメータのうち、値が `pred` に合格するもの以外を取り除く。 15 | * もし `limit` に数値が指定されている場合は、その数を超過した分を取り除く。 16 | * 17 | * それ以外の `key` についてはそのまま。 18 | * @example 19 | * ``` 20 | * const query = { 21 | * a: ["abb", "axa", "aaa"], 22 | * b: "bbb" 23 | * } 24 | * filterQueryParams("a", (s) => s.endsWith("a"), { limit: 1 })(query) 25 | * // -> { a: ["axa"], b: "bbb" } 26 | * ``` 27 | */ 28 | export const filterQueryParams = ( 29 | key: string, 30 | pred: (s: string) => boolean, 31 | options: { limit?: number } = {}, 32 | ): QueryMutation => { 33 | const { limit = undefined } = options; 34 | 35 | return queryMutation((query) => { 36 | const _value = query[key]; 37 | if (!_value) return query; 38 | 39 | const value: string[] = typeof _value === "string" ? [_value] : _value; 40 | 41 | type Predicate = Parameters["filter"]>[0]; 42 | const limitter: Predicate = limit === undefined 43 | ? () => true 44 | : (_, index) => index < limit; 45 | return { ...query, [key]: value.filter(pred).filter(limitter) }; 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /mutators/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | export { filterQueryParams } from "./filterQueryParams.ts"; 7 | export { pushQueryParams } from "./pushQueryParams.ts"; 8 | export { resetQuery } from "./resetQuery.ts"; 9 | export { removeQueryParams } from "./removeQueryParams.ts"; 10 | export { setQueryParams } from "./setQueryParams.ts"; 11 | -------------------------------------------------------------------------------- /mutators/pushQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { pushQueryParams } from "./pushQueryParams.ts"; 4 | 5 | describe("pushQueryParams(params)", () => { 6 | type Params = Parameters[0]; 7 | type Case = [params: Params, query: ParsedUrlQuery, result: ParsedUrlQuery]; 8 | 9 | ([ 10 | [{}, {}, {}], 11 | [{ a: "a" }, {}, { a: ["a"] }], 12 | [{}, { a: "a" }, { a: "a" }], 13 | [{ a: ["a"] }, {}, { a: ["a"] }], 14 | [{}, { a: ["a"] }, { a: ["a"] }], 15 | [{ a: "b" }, { a: "a" }, { a: ["a", "b"] }], 16 | [{ a: ["b"] }, { a: "a" }, { a: ["a", "b"] }], 17 | [{ a: "b" }, { a: ["a"] }, { a: ["a", "b"] }], 18 | [{ a: ["b"] }, { a: ["a"] }, { a: ["a", "b"] }], 19 | ] satisfies Case[]).forEach(([params, query, result]) => { 20 | it(`(${JSON.stringify(params)})(${JSON.stringify(query)}) === ${JSON.stringify(result)}`, () => { 21 | assertEquals(pushQueryParams(params)(query), result); 22 | }); 23 | }); 24 | 25 | ([ 26 | [{ a: "a" }, { b: "b" }, { a: ["a"], b: "b" }], 27 | [ 28 | { a: "a", b: "b" }, 29 | { a: "aa", b: "bb" }, 30 | { a: ["aa", "a"], b: ["bb", "b"] }, 31 | ], 32 | ] satisfies Case[]).forEach(([params, query, result]) => { 33 | it(`(${JSON.stringify(params)})(${JSON.stringify(query)}) === ${JSON.stringify(result)}`, () => { 34 | assertEquals(pushQueryParams(params)(query), result); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /mutators/pushQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | /** 7 | * Appends given parameters. 8 | * 9 | * 末尾にパラメータを追加する。 10 | * 11 | * @example 12 | * ``` 13 | * const query = { 14 | * a: "a", 15 | * b: "b" 16 | * } 17 | * pushQueryParams({ a: "aa", c: "c" })(query) 18 | * // -> { a: ["a", "aa"], b: "b", c: "c" } 19 | * ``` 20 | */ 21 | export const pushQueryParams = ( 22 | params: Record, 23 | ): QueryMutation => { 24 | return queryMutation((query) => 25 | Object.entries(params).reduce( 26 | (acc, [key, value]) => ({ ...acc, [key]: safeConcat(acc[key], value) }), 27 | query, 28 | ) 29 | ); 30 | }; 31 | 32 | type ParamToAdd = Parameters[0][string]; 33 | 34 | const safeConcat = ( 35 | left: string | string[] | undefined, 36 | right: ParamToAdd, 37 | ): string[] => [...toFlatArray(left), ...toFlatArray(right)]; 38 | 39 | const toFlatArray = (v: ParamToAdd): string[] => 40 | !v ? [] : typeof v === "string" ? [v] : v; 41 | -------------------------------------------------------------------------------- /mutators/removeQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { removeQueryParams } from "./removeQueryParams.ts"; 4 | 5 | describe("removeQueryParams(options)(query)", () => { 6 | type Case = [ 7 | options: Parameters[0], 8 | query: ParsedUrlQuery, 9 | result: ParsedUrlQuery, 10 | ]; 11 | ([ 12 | [{}, {}, {}], 13 | [{ a: true }, { a: "aaa" }, { a: [] }], 14 | [{ a: true }, { a: ["aaa", "bbb"] }, { a: [] }], 15 | [{ a: false }, { a: "aaa" }, { a: "aaa" }], 16 | [{ a: false }, { a: ["aaa", "bbb"] }, { a: ["aaa", "bbb"] }], 17 | [{ a: [] }, { a: ["aaa", "bbb"] }, { a: ["aaa", "bbb"] }], 18 | [{ a: "" }, { a: ["aaa", "bbb"] }, { a: ["aaa", "bbb"] }], 19 | [{ a: "aaa" }, { a: ["aaa", "bbb"] }, { a: ["bbb"] }], 20 | [{ a: ["aaa"] }, { a: ["aaa", "bbb"] }, { a: ["bbb"] }], 21 | [ 22 | { a: ["aaa"] }, 23 | { a: ["aaa", "bbb"], other: "ccc" }, 24 | { a: ["bbb"], other: "ccc" }, 25 | ], 26 | [{ a: "aaa" }, { other: "ccc" }, { other: "ccc" }], 27 | ] satisfies Case[]).forEach(([options, query, result]) => { 28 | it(`(${JSON.stringify(options)})(${JSON.stringify(query)}) === ${JSON.stringify(result)}`, () => { 29 | assertEquals(removeQueryParams(options)(query), result); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /mutators/removeQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | /** 7 | * By passing `({ key0: predicate0, key1: predicate1, ... })` style object, 8 | * removes values for key from query object. 9 | * 10 | * You can pass predicates from one of them; 11 | * 12 | * --- 13 | * 14 | * `({ key0: 述語_0, key1: 述語_1, ... })` の形のオブジェクトを渡すことで 15 | * それらの key に対応する値をクエリオブジェクトから取り除きます。 16 | * 17 | * 渡すことのできる *述語* は次の通り; 18 | * 19 | * --- 20 | * 21 | * 1\. `true` — to remove all for the key
22 | * (すべての値を取り除く) 23 | * 24 | * ``` 25 | * const query = { a: ["aaa", "abb"] } 26 | * removeQueryParams({ a: true })(query) 27 | * // -> { a: [] } 28 | * ``` 29 | * 30 | * 2\. `false`, `undefined`, `null`, or `""` — **not** to remove anything for the key
31 | * (なにも取り除かない) 32 | * 33 | * ``` 34 | * const query = { a: ["aaa", "abb"] } 35 | * removeQueryParams({ a: false })(query) 36 | * // -> { a: ["aaa", "abb"] } 37 | * ``` 38 | * 39 | * 3\. *single string* — to remove it
40 | * (その値を取り除く) 41 | * 42 | * ``` 43 | * const query = { a: ["aaa", "abb"] } 44 | * removeQueryParams({ a: "aaa" })(query) 45 | * // -> { a: ["abb"] } 46 | * ``` 47 | * 48 | * 4\. *array of string* — to remove all of them
49 | * (全て取り除く) 50 | * ``` 51 | * const query = { a: ["aaa", "abb", "acc"] } 52 | * removeQueryParams({ a: ["aaa", "abb"] })(query) 53 | * // -> { a: ["acc"] } 54 | * ``` 55 | */ 56 | export const removeQueryParams = ( 57 | options: Record, 58 | ): QueryMutation => { 59 | return queryMutation((query) => 60 | Object.entries(options).reduce((acc, [key, pred]) => { 61 | const value = acc[key]; 62 | 63 | // if empty, leave query as it is. 64 | if (!value) return acc; 65 | if (Array.isArray(value) && value.length === 0) return acc; 66 | 67 | const predFn = toFn(pred); 68 | 69 | // if array of string 70 | if (Array.isArray(value)) return { ...acc, [key]: value.filter(predFn) }; 71 | 72 | // if single string (not empty) 73 | return { ...acc, [key]: predFn(value) ? value : [] }; 74 | }, query) 75 | ); 76 | }; 77 | 78 | type RemovingPredicate = Parameters[0][string]; 79 | 80 | /** 81 | * convert shorthand predicate into function 82 | * 83 | * @internal 84 | */ 85 | const toFn = (p: RemovingPredicate): (s: string) => boolean => { 86 | if (!p) return () => true; 87 | if (p === true) return () => false; 88 | if (Array.isArray(p)) return (s) => !p.includes(s); 89 | return (s) => s !== p; 90 | }; 91 | -------------------------------------------------------------------------------- /mutators/resetQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { resetQuery } from "./resetQuery.ts"; 4 | 5 | describe("resetQuery(options)(query)", () => { 6 | type Case = [ 7 | options: Parameters[0], 8 | query: ParsedUrlQuery, 9 | result: ParsedUrlQuery, 10 | ]; 11 | 12 | ([ 13 | [{}, /* */ { a: "aaa" }, {}], 14 | [{ ignore: "" }, { a: "aaa" }, {}], 15 | [{ ignore: [] }, { a: "aaa" }, {}], 16 | [{ ignore: [false] }, { a: "aaa" }, {}], 17 | [{ ignore: ["b"] }, { a: "aaa" }, {}], 18 | [{ ignore: "a" }, { a: "aaa" }, { a: "aaa" }], 19 | [{ ignore: ["a"] }, { a: "aaa" }, { a: "aaa" }], 20 | [{ ignore: ["a", "b"] }, { a: "aaa" }, { a: "aaa" }], 21 | [{ ignore: ["a", "b"] }, { a: "aaa", b: "bbb" }, { a: "aaa", b: "bbb" }], 22 | [{ ignore: ["a", false] }, { a: "aaa", b: "bbb" }, { a: "aaa" }], 23 | ] satisfies Case[]).forEach(([options, query, result]) => { 24 | it(`(${JSON.stringify(options)})(${JSON.stringify(query)}) === ${JSON.stringify(result)}`, () => { 25 | assertEquals(resetQuery(options)(query), result); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /mutators/resetQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | /** 7 | * Resets the query (into empty object). 8 | * 9 | * Keys specified in `ignore` option **will not** be deleted. 10 | * 11 | * Falsy value or *boolean value*s in the array will be discarded. 12 | * 13 | * --- 14 | * 15 | * クエリをリセットして、空のオブジェクトにします。 16 | * 17 | * `ignore` オプションに指定したキーについては **リセットされず** に残ります。 18 | * 19 | * Falsy な値(偽値)や、**真偽値** は無視されます。 20 | * ``` 21 | * const query = { a: "aaa", b: "bbb" } 22 | * resetQuery({ ignore: "a" })(query) // -> { a: "aaa" } 23 | * resetQuery({ ignore: ["a", "meaningless"] }) // -> { a: "aaa" } 24 | * 25 | * resetQuery({ ignore: ["a", true, false, 0, null] })(query) // -> { a: "aaa" } 26 | * ``` 27 | */ 28 | export const resetQuery = ( 29 | options: { 30 | /** 31 | * keys not to delete. 32 | * Falsy value or `true` will be discarded. 33 | */ 34 | ignore?: 35 | | string 36 | | (string | undefined | null | boolean | number)[] 37 | | undefined; 38 | } = {}, 39 | ): QueryMutation => { 40 | const ignoredKeys = (() => { 41 | const { ignore } = options; 42 | if (!ignore) return []; 43 | if (Array.isArray(ignore)) { 44 | return ignore.filter((k): k is string => !!k && k !== true); 45 | } 46 | return [ignore]; 47 | })(); 48 | 49 | return queryMutation((q) => { 50 | const entries = ignoredKeys 51 | .map((key) => [key, q[key]] as const) 52 | .filter(([, v]) => !!v); 53 | 54 | return Object.fromEntries(entries); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /mutators/setQueryParams.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, describe, it } from "../test/index.ts"; 2 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 3 | import { setQueryParams } from "./setQueryParams.ts"; 4 | 5 | describe("setQueryParams(other)(query)", () => { 6 | type Case = [ 7 | params: Parameters[0], 8 | query: ParsedUrlQuery, 9 | result: ParsedUrlQuery, 10 | ]; 11 | 12 | ([ 13 | [{ a: "aaa" }, { b: "bbb" }, { a: "aaa", b: "bbb" }], 14 | [{ a: "aaa" }, { a: "abb" }, { a: "aaa" }], 15 | [{ a: undefined }, { a: "abb" }, { a: [] }], 16 | [{ a: "" }, /* */ { a: "abb" }, { a: [] }], 17 | [{ a: null }, /**/ { a: "abb" }, { a: [] }], 18 | [{ a: 0 }, /* */ { a: "abb" }, { a: [] }], 19 | [{}, /* */ { a: "aaa" }, { a: "aaa" }], 20 | [{ a: ["abb", "acc"] }, { a: "aaa" }, { a: ["abb", "acc"] }], 21 | ] satisfies Case[]).forEach(([params, query, result]) => { 22 | it(`(${JSON.stringify(params)})(${JSON.stringify(query)}) === ${JSON.stringify(result)}`, () => { 23 | assertEquals(setQueryParams(params)(query), result); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /mutators/setQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryMutation, 3 | queryMutation, 4 | } from "./_internal/queryMutation.ts"; 5 | 6 | /** 7 | * Sets values of query parameters. 8 | * 9 | * for each key in `params` object, 10 | * 11 | * - if value is *falsy* 12 | * ... resets the values for the key. 13 | * - *otherwise* 14 | * ... replace old values for the key 15 | * with the given value 16 | * 17 | * --- 18 | * 19 | * クエリに値をセットします。 20 | * 21 | * `params` オブジェクトの各キーについて、 22 | * 23 | * - 値が *falsy* (偽値)の場合 24 | * ... そのキーの値をリセットします。 25 | * - *それ以外の場合* 26 | * ... そのキーに対して、古い値は捨てて新しい値をセットします。 27 | * 28 | * @example 29 | * ``` 30 | * const query = { a: "aaa" } 31 | * 32 | * setQueryParams({ a: "abc" })(query) // -> { a: "abc" } 33 | * setQueryParams({ b: "bbb" })(query) // -> { a: "aaa", b: "bbb" } 34 | * setQueryParams({})(query) // -> { a: "aaa" } 35 | * 36 | * // falsy values like: 0 | null | undefined | false | "" 37 | * setQueryParams({ a: null })(query) // -> { a: [] } 38 | * ``` 39 | */ 40 | export const setQueryParams = ( 41 | params: Record, 42 | ): QueryMutation => { 43 | return queryMutation((query) => 44 | Object.entries(params).reduce( 45 | (acc, [key, value]) => ({ 46 | ...acc, 47 | [key]: !value ? [] : value, 48 | }), 49 | query, 50 | ) 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /predicates/index.ts: -------------------------------------------------------------------------------- 1 | export { isQueryEmpty } from "./isQueryEmpty.ts"; 2 | -------------------------------------------------------------------------------- /predicates/isQueryEmpty.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertFalse, describe, it } from "../test/index.ts"; 2 | import { isQueryEmpty } from "./isQueryEmpty.ts"; 3 | 4 | type Case = Parameters; 5 | 6 | describe("isQueryPristine(query, options)", () => { 7 | ([ 8 | [{ a: undefined }, undefined], 9 | [{ a: [] }, undefined], 10 | [{ a: [] }, { ignore: "" }], 11 | [{ a: [] }, { ignore: [] }], 12 | [{ a: "aaa" }, { ignore: "a" }], 13 | [{ a: "aaa" }, { ignore: ["a"] }], 14 | [{ a: "aaa" }, { ignore: ["a", "b"] }], 15 | [{ a: "aaa", b: "bbb" }, { ignore: ["a", "b"] }], 16 | ] satisfies Case[]).forEach(([query, options]) => { 17 | it(`(${JSON.stringify(query)}, ${JSON.stringify(options)}) === true`, () => { 18 | assert(isQueryEmpty(query, options)); 19 | }); 20 | }); 21 | 22 | ([ 23 | [{ a: "aaa" }, undefined], 24 | [{ a: "aaa" }, { ignore: "b" }], 25 | [{ a: "aaa", b: "bbb" }, { ignore: "a" }], 26 | ] satisfies Case[]).forEach(([query, options]) => { 27 | it(`(${JSON.stringify(query)}, ${JSON.stringify(options)}) === false`, () => { 28 | assertFalse(isQueryEmpty(query, options)); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /predicates/isQueryEmpty.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedUrlQuery } from "../types/ParsedUrlQuery.ts"; 2 | 3 | /** 4 | * Returns true if the query is *empty* while 5 | * `undefined`, `""`, and `[]` value will be ignored. 6 | * 7 | * If `ignore` option is specified, the keys will be ignored. 8 | * 9 | * --- 10 | * 11 | * クエリが *空* であるかどうかの真偽値を返す。 12 | * ただし、`undefined`, `""`, and `[]` があっても無視する。 13 | * 14 | * もし `ignore` オプションが指定されている場合、指定したキーの値があっても無視する。 15 | * @example 16 | * ``` 17 | * isQueryPristine({}) // -> true 18 | * isQueryPristine({ id: "a" }, { ignore: "id" }) // -> true 19 | * ``` 20 | */ 21 | export const isQueryEmpty = ( 22 | query: ParsedUrlQuery, 23 | options: { 24 | ignore?: string | string[] | undefined | null; 25 | } = {}, 26 | ): boolean => { 27 | const keysToIgnore = toArrayKeysToIgnore(options.ignore); 28 | return Object.entries(query).every(([k, v]) => { 29 | if (keysToIgnore.includes(k)) return true; 30 | if (Array.isArray(v)) return v.length === 0; 31 | return !v; 32 | }); 33 | }; 34 | 35 | const toArrayKeysToIgnore = ( 36 | ignore: string | string[] | null | undefined, 37 | ): string[] => { 38 | if (Array.isArray(ignore)) return ignore; 39 | if (!ignore) return []; 40 | return [ignore]; 41 | }; 42 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | export { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; 2 | export { 3 | assert, 4 | assertEquals, 5 | assertFalse, 6 | assertStrictEquals, 7 | } from "@std/assert"; 8 | -------------------------------------------------------------------------------- /types/ParsedUrlQuery.ts: -------------------------------------------------------------------------------- 1 | export type ParsedUrlQuery = Record; 2 | --------------------------------------------------------------------------------