All files URLSearchParams.ts

100% Statements 44/44
100% Branches 20/20
100% Functions 8/8
100% Lines 44/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158  6x   6x                                                         6x 66x 2x 64x 42x     117x 108x     22x 14x 38x     8x     6x 22x 22x 15x   7x                                                                     6x 11x 11x 26x 16x 8x   16x 25x       11x                                               6x 11x 11x 23x 12x 8x   12x 26x       11x             6x 24x 6x 6x 6x 18x 12x   6x      
import { JSONPrimitive } from '@battis/typescript-tricks';
import * as String from './String.js';
import { ish as URLish } from './URL.js';
import { isJSONEntries, isJSONRecord } from './is.js';
 
export type ish =
  | URLSearchParams
  /* exception thrown for string[][] that that doesn't match [string,string][] **/
  | Exclude<ConstructorParameters<typeof URLSearchParams>[0], string[][]>
  | Record<string, JSONPrimitive | undefined>
  | [string, JSONPrimitive | undefined][];
 
/**
 * Construct an actual URLSearchParams object from a URLSearchParams.ish
 *
 * `undefined` values are removed from records:
 *
 * ```ts
 * from({ a: null, b: undefined, c: '' }); // a=null&c=
 * ```
 *
 * Entry arrays allow undefined values and multiple values per key:
 *
 * ```ts
 * from([
 *   ['a', 'A'],
 *   ['a', 'B'],
 *   ['c', undefined],
 *   ['d', null]
 * ]); // a=A&a=B&c=&d=null
 * ```
 */
export function from(search?: ish): URLSearchParams {
  if (search instanceof URLSearchParams) {
    return search;
  } else if (isJSONRecord(search)) {
    return new URLSearchParams(
      Object.fromEntries(
        Object.entries(search)
          .filter(([__dirname, value]) => value !== undefined)
          .map(([key, value]) => [key, String.from(value)])
      )
    );
  } else if (isJSONEntries(search)) {
    return new URLSearchParams(
      search.map(([key, value]) => [key, String.from(value)])
    );
  }
  return new URLSearchParams(search);
}
 
export function toString(search: ish): string {
  const query = from(search).toString();
  if (query.length) {
    return `?${query}`;
  }
  return '';
}
 
/**
 * Merge a collection of URLSearchParams.ish into a single URLSearchParams
 * object
 *
 * `URLSearchParams.ish` are instantiated using `from()` before merging`:
 *
 * ```ts
 * merge({ a: 1, b: undefined, c: 'foo' }); // a=1&c=foo
 * merge([
 *   ['a', 1],
 *   ['b', undefined],
 *   ['c', 'foo']
 * ]); // a=1&b=&c=foo
 * ```
 *
 * Duplicate key values overwrite each other, even in a single
 * URLSearchParams.ish:
 *
 * ```ts
 * merge([
 *   ['a', 'A'],
 *   ['a', 'B']
 * ]); // a=B
 * ```
 *
 * `URLSearchParams.ish` are merged in order, overwriting previous key values:
 *
 * ```ts
 * merge({ a: 1, b: 2 }, { b: 3, c: 4 }, { c: 5, d: 6 }); // a=1&b=3&c=5&d=6
 * merge({ a: 1 }, { a: undefined, b: 2, c: 3 }, { b: undefined, c: 4 }); // a=1&b=2&c=4
 * ```
 */
export function merge(...sources: ish[]): URLSearchParams | undefined {
  let search: URLSearchParams | undefined = undefined;
  for (const source of sources) {
    if (source) {
      if (!search) {
        search = new URLSearchParams();
      }
      for (const [key, value] of from(source).entries()) {
        search.set(key, value);
      }
    }
  }
  return search;
}
 
/**
 * Concatenate `URLSearchParams.ish` together in order without overwriting
 * previous key-value pairs
 *
 * `URLSearchParams.ish` are instantiated using `from()` before concatenation`:
 *
 * ```ts
 * concatenate({ a: 1, b: undefined, c: 'foo' }); // a=1&c=2
 * concatenate([
 *   ['a', 1],
 *   ['b', undefined],
 *   ['c', 'foo']
 * ]); // a=1&b=&c=foo
 * ```
 *
 * Repeated keys are append to the query:
 *
 * ```ts
 * concatenate({ a: 1, b: 2 }, { b: 3, c: 4 }, { c: 5, d: 6 }); // a=1&b=2&b=3&c=4&c=5&d=6
 * ```
 */
export function concatenate(...sources: ish[]): URLSearchParams | undefined {
  let search: URLSearchParams | undefined = undefined;
  for (const source of sources) {
    if (source) {
      if (!search) {
        search = new URLSearchParams();
      }
      for (const [key, value] of from(source).entries()) {
        search.append(key, value);
      }
    }
  }
  return search;
}
 
/**
 * If the `URLSearchParams.ish` is defined, append it to the `URL.ish` replacing
 * any existing query string, otherwise return the `URL.ish` unmodified.
 */
export function appendTo(url: URLish, search: ish): URLish {
  if (url instanceof URL) {
    const result = new URL(url);
    result.search = toString(search);
    return result;
  } else if (search) {
    return url.replace(/^([^?]+)(\?.*)?$/, `$1${toString(search)}`);
  } else {
    return url;
  }
}