@warp-drive-mirror/utilities 5.8.0-beta.0 → 5.8.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +16 -25
  2. package/declarations/-private/active-record/find-record.d.ts +5 -6
  3. package/declarations/-private/active-record/query.d.ts +2 -2
  4. package/declarations/-private/active-record/save-record.d.ts +2 -2
  5. package/declarations/-private/handlers/meta-doc.d.ts +1 -1
  6. package/declarations/-private/json-api/find-record.d.ts +37 -18
  7. package/declarations/-private/json-api/query.d.ts +3 -6
  8. package/declarations/-private/json-api/save-record.d.ts +45 -10
  9. package/declarations/-private/json-api/serialize.d.ts +15 -1
  10. package/declarations/-private/rest/find-record.d.ts +5 -8
  11. package/declarations/-private/rest/query.d.ts +2 -2
  12. package/declarations/-private/rest/save-record.d.ts +2 -2
  13. package/dist/active-record.js +2 -0
  14. package/dist/handlers.js +3 -3
  15. package/dist/json-api.js +94 -26
  16. package/dist/rest.js +2 -2
  17. package/dist/string.js +428 -1
  18. package/dist/unpkg/dev/-private.js +7 -0
  19. package/dist/unpkg/dev/active-record.js +394 -0
  20. package/dist/unpkg/dev/builder-utils-Donkk-BZ.js +22 -0
  21. package/dist/unpkg/dev/derivations.js +30 -0
  22. package/dist/unpkg/dev/handlers.js +316 -0
  23. package/dist/unpkg/dev/index.js +362 -0
  24. package/dist/unpkg/dev/inflect-BEv8WqY1.js +343 -0
  25. package/dist/unpkg/dev/json-api.js +740 -0
  26. package/dist/unpkg/dev/rest.js +392 -0
  27. package/dist/unpkg/dev/string.js +1 -0
  28. package/dist/unpkg/dev-deprecated/-private.js +7 -0
  29. package/dist/unpkg/dev-deprecated/active-record.js +394 -0
  30. package/dist/unpkg/dev-deprecated/builder-utils-Donkk-BZ.js +22 -0
  31. package/dist/unpkg/dev-deprecated/derivations.js +30 -0
  32. package/dist/unpkg/dev-deprecated/handlers.js +316 -0
  33. package/dist/unpkg/dev-deprecated/index.js +362 -0
  34. package/dist/unpkg/dev-deprecated/inflect-BEv8WqY1.js +343 -0
  35. package/dist/unpkg/dev-deprecated/json-api.js +740 -0
  36. package/dist/unpkg/dev-deprecated/rest.js +392 -0
  37. package/dist/unpkg/dev-deprecated/string.js +1 -0
  38. package/dist/unpkg/prod/-private.js +7 -0
  39. package/dist/unpkg/prod/active-record.js +366 -0
  40. package/dist/unpkg/prod/builder-utils-Donkk-BZ.js +22 -0
  41. package/dist/unpkg/prod/derivations.js +30 -0
  42. package/dist/unpkg/prod/handlers.js +305 -0
  43. package/dist/unpkg/prod/index.js +239 -0
  44. package/dist/unpkg/prod/inflect-Dh9dyEYx.js +333 -0
  45. package/dist/unpkg/prod/json-api.js +702 -0
  46. package/dist/unpkg/prod/rest.js +364 -0
  47. package/dist/unpkg/prod/string.js +1 -0
  48. package/dist/unpkg/prod-deprecated/-private.js +7 -0
  49. package/dist/unpkg/prod-deprecated/active-record.js +366 -0
  50. package/dist/unpkg/prod-deprecated/builder-utils-Donkk-BZ.js +22 -0
  51. package/dist/unpkg/prod-deprecated/derivations.js +30 -0
  52. package/dist/unpkg/prod-deprecated/handlers.js +305 -0
  53. package/dist/unpkg/prod-deprecated/index.js +239 -0
  54. package/dist/unpkg/prod-deprecated/inflect-Dh9dyEYx.js +333 -0
  55. package/dist/unpkg/prod-deprecated/json-api.js +702 -0
  56. package/dist/unpkg/prod-deprecated/rest.js +364 -0
  57. package/dist/unpkg/prod-deprecated/string.js +1 -0
  58. package/logos/README.md +2 -2
  59. package/logos/logo-yellow-slab.svg +1 -0
  60. package/logos/word-mark-black.svg +1 -0
  61. package/logos/word-mark-white.svg +1 -0
  62. package/package.json +13 -5
  63. package/logos/NCC-1701-a-blue.svg +0 -4
  64. package/logos/NCC-1701-a-gold.svg +0 -4
  65. package/logos/NCC-1701-a-gold_100.svg +0 -1
  66. package/logos/NCC-1701-a-gold_base-64.txt +0 -1
  67. package/logos/NCC-1701-a.svg +0 -4
  68. package/logos/docs-badge.svg +0 -2
  69. package/logos/ember-data-logo-dark.svg +0 -12
  70. package/logos/ember-data-logo-light.svg +0 -12
  71. package/logos/social1.png +0 -0
  72. package/logos/social2.png +0 -0
  73. package/logos/warp-drive-logo-dark.svg +0 -4
  74. package/logos/warp-drive-logo-gold.svg +0 -4
@@ -0,0 +1,305 @@
1
+ function isCompressibleMethod(method) {
2
+ return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';
3
+ }
4
+
5
+ /**
6
+ * Whether the browser supports `ReadableStream` as a request body
7
+ * in a `POST` request.
8
+ *
9
+ * @group Constants
10
+ */
11
+ const SupportsRequestStreams = (() => {
12
+ let duplexAccessed = false;
13
+ const hasContentType = new Request('', {
14
+ body: new ReadableStream(),
15
+ method: 'POST',
16
+ // @ts-expect-error untyped
17
+ get duplex() {
18
+ duplexAccessed = true;
19
+ return 'half';
20
+ }
21
+ }).headers.has('Content-Type');
22
+ return duplexAccessed && !hasContentType;
23
+ })();
24
+
25
+ /**
26
+ * Options for configuring the AutoCompress handler.
27
+ *
28
+ */
29
+
30
+ const DEFAULT_CONSTRAINTS = {
31
+ Blob: 1000,
32
+ ArrayBuffer: 1000,
33
+ TypedArray: 1000,
34
+ DataView: 1000,
35
+ String: 1000
36
+ };
37
+ const TypedArray = Object.getPrototypeOf(Uint8Array);
38
+
39
+ /**
40
+ * A request handler that automatically compresses the request body
41
+ * if the request body is a string, array buffer, blob, or form data.
42
+ *
43
+ * This uses the [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)
44
+ *
45
+ * The compression format as well as the kinds of data to compress can be
46
+ * configured using the `format` and `constraints` options.
47
+ *
48
+ * ```diff
49
+ * +import { AutoCompress } from '@ember-data-mirror/request-utils/handlers';
50
+ * import Fetch from '@ember-data-mirror/request/fetch';
51
+ * import RequestManager from '@ember-data-mirror/request';
52
+ * import Store from '@ember-data-mirror/store';
53
+ *
54
+ * class AppStore extends Store {
55
+ * requestManager = new RequestManager()
56
+ * .use([
57
+ * + new AutoCompress(),
58
+ * Fetch
59
+ * ]);
60
+ * }
61
+ * ```
62
+ *
63
+ * @group Handlers
64
+ * @public
65
+ * @since 5.5.0
66
+ */
67
+ class AutoCompress {
68
+ constructor(options = {}) {
69
+ const opts = {
70
+ format: options.format ?? 'gzip',
71
+ constraints: Object.assign({}, DEFAULT_CONSTRAINTS, options.constraints),
72
+ allowStreaming: options.allowStreaming ?? false,
73
+ forceStreaming: options.forceStreaming ?? false
74
+ };
75
+ this.options = opts;
76
+ }
77
+ request({
78
+ request
79
+ }, next) {
80
+ const {
81
+ constraints
82
+ } = this.options;
83
+ const {
84
+ body
85
+ } = request;
86
+ const shouldCompress = !!body && isCompressibleMethod(request.method) && request.options?.compress !== false && (
87
+ // prettier-ignore
88
+ request.options?.compress ? true : typeof body === 'string' || body instanceof String ? canCompress('String', constraints, body.length) : body instanceof Blob ? canCompress('Blob', constraints, body.size) : body instanceof ArrayBuffer ? canCompress('ArrayBuffer', constraints, body.byteLength) : body instanceof DataView ? canCompress('DataView', constraints, body.byteLength) : body instanceof TypedArray ? canCompress('TypedArray', constraints, body.byteLength) : false);
89
+ if (!shouldCompress) return next(request);
90
+
91
+ // A convenient way to convert all of the supported body types to a readable
92
+ // stream is to use a `Response` object body
93
+ const response = new Response(request.body);
94
+ const stream = response.body?.pipeThrough(new CompressionStream(this.options.format));
95
+ const headers = new Headers(request.headers);
96
+ headers.set('Content-Encoding', encodingForFormat(this.options.format));
97
+
98
+ //
99
+ // For browsers that support it, `fetch` can receive a `ReadableStream` as
100
+ // the body, so all we need to do is to create a new `ReadableStream` and
101
+ // compress it on the fly
102
+ //
103
+ const forceStreaming = request.options?.forceStreaming ?? this.options.forceStreaming;
104
+ const allowStreaming = request.options?.allowStreaming ?? this.options.allowStreaming;
105
+ if (forceStreaming || SupportsRequestStreams && allowStreaming) {
106
+ const req = Object.assign({}, request, {
107
+ body: stream,
108
+ headers
109
+ });
110
+ if (SupportsRequestStreams) {
111
+ // @ts-expect-error untyped
112
+ req.duplex = 'half';
113
+ }
114
+ return next(req);
115
+
116
+ //
117
+ // For non-chromium browsers, we have to "pull" the stream to get the final
118
+ // bytes and supply the final byte array as the new request body.
119
+ //
120
+ }
121
+
122
+ // we need to pull the stream to get the final bytes
123
+ const resp = new Response(stream);
124
+ return resp.blob().then(blob => {
125
+ const req = Object.assign({}, request, {
126
+ body: blob,
127
+ headers
128
+ });
129
+ return next(req);
130
+ });
131
+ }
132
+ }
133
+ function canCompress(type, constraints, size) {
134
+ // if we have a value of 0, we can compress anything
135
+ if (constraints[type] === 0) return true;
136
+ if (constraints[type] === -1) return false;
137
+ return size >= constraints[type];
138
+ }
139
+ function encodingForFormat(format) {
140
+ switch (format) {
141
+ case 'gzip':
142
+ case 'deflate':
143
+ case 'deflate-raw':
144
+ return format;
145
+ default:
146
+ // @ts-expect-error - unreachable code is reachable in production
147
+ return format;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * If CheckFn returns true, the wrapped handler will be used.
153
+ * If CheckFn returns false, the wrapped handler will be skipped.
154
+ */
155
+
156
+ /**
157
+ *
158
+ * @group Handlers
159
+ * @public
160
+ */
161
+ class Gate {
162
+ constructor(handler, checkFn) {
163
+ this.handler = handler;
164
+ this.checkFn = checkFn;
165
+ }
166
+ request(context, next) {
167
+ if (this.checkFn(context)) {
168
+ return this.handler.request(context, next);
169
+ }
170
+ return next(context.request);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * MetaDocHandler processes requests that are marked as meta requests.
176
+ *
177
+ * It treats the response body as "entirely meta" transforming
178
+ *
179
+ * ```ts
180
+ * {
181
+ * some: "key",
182
+ * another: "thing"
183
+ * }
184
+ * ```
185
+ *
186
+ * into
187
+ *
188
+ * ```ts
189
+ * {
190
+ * meta: {
191
+ * some: "key",
192
+ * another: "thing"
193
+ * }
194
+ * }
195
+ * ```
196
+ *
197
+ * To activate this handler, a request should specify
198
+ *
199
+ * ```ts
200
+ * options.isMetaRequest = true
201
+ * ```
202
+ *
203
+ * For instance
204
+ *
205
+ * ```ts
206
+ * store.request({
207
+ * url: '/example',
208
+ * options: {
209
+ * isMetaRequest: true
210
+ * }
211
+ * });
212
+ * ```
213
+ *
214
+ * Errors are not processed by this handler, so if the request fails and the error response
215
+ * is not in {json:api} format additional processing may be needed.
216
+ *
217
+ * @group Handlers
218
+ */
219
+ const MetaDocHandler = {
220
+ request(context, next) {
221
+ if (!context.request.options?.isMetaRequest) {
222
+ return next(context.request);
223
+ }
224
+ return next(context.request).then(response => {
225
+ return processResponse(response);
226
+ });
227
+ }
228
+ };
229
+ function processResponse(response) {
230
+ return {
231
+ meta: response.content
232
+ };
233
+ }
234
+ if (typeof FastBoot === 'undefined') {
235
+ globalThis.addEventListener('beforeunload', function () {
236
+ sessionStorage.setItem('tab-closed', 'true');
237
+ });
238
+ }
239
+ function getTabId() {
240
+ if (typeof sessionStorage === 'undefined') {
241
+ return crypto.randomUUID();
242
+ }
243
+ const tabId = sessionStorage.getItem('tab-id');
244
+ if (tabId) {
245
+ const tabClosed = sessionStorage.getItem('tab-closed');
246
+ if (tabClosed === 'true') {
247
+ return tabId;
248
+ }
249
+
250
+ // fall through to generate a new tab id
251
+ }
252
+ const newTabId = crypto.randomUUID();
253
+ sessionStorage.setItem('tab-id', newTabId);
254
+ return newTabId;
255
+ }
256
+
257
+ /**
258
+ * A unique identifier for the current browser tab
259
+ * useful for observability/tracing and deduping
260
+ * across multiple tabs.
261
+ *
262
+ * @group Constants
263
+ */
264
+ const TAB_ID = getTabId();
265
+ /**
266
+ * The epoch seconds at which the tab id was generated
267
+ *
268
+ * @group Constants
269
+ */
270
+ const TAB_ASSIGNED = Math.floor(Date.now() / 1000);
271
+
272
+ /**
273
+ * Adds the `X-Amzn-Trace-Id` header to support observability
274
+ * tooling around request routing.
275
+ *
276
+ * This makes use of the {@link TAB_ID} and {@link TAB_ASSIGNED}
277
+ * to enable tracking the browser tab of origin across multiple requests.
278
+ *
279
+ * Follows the template: `Root=1-${now}-${uuidv4};TabId=1-${epochSeconds}-${tab-uuid}`
280
+ *
281
+ * @group Utility Functions
282
+ */
283
+ function addTraceHeader(headers) {
284
+ const now = Math.floor(Date.now() / 1000);
285
+ headers.set('X-Amzn-Trace-Id', `Root=1-${now}-${crypto.randomUUID()};TabId=1-${TAB_ASSIGNED}-${TAB_ID}`);
286
+ return headers;
287
+ }
288
+
289
+ /**
290
+ * Source: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html
291
+ * As of 2024-12-05 the maximum URL length is 8192 bytes.
292
+ *
293
+ * @group Constants
294
+ */
295
+ const MAX_URL_LENGTH = 8192;
296
+
297
+ /**
298
+ * This assertion takes a URL and throws an error if the URL is longer than the maximum URL length.
299
+ *
300
+ * See also {@link MAX_URL_LENGTH}
301
+ *
302
+ * @group Utility Functions
303
+ */
304
+ function assertInvalidUrlLength(url) {}
305
+ export { AutoCompress, Gate, MAX_URL_LENGTH, MetaDocHandler, SupportsRequestStreams, TAB_ASSIGNED, TAB_ID, addTraceHeader, assertInvalidUrlLength };
@@ -0,0 +1,239 @@
1
+ import { getOrSetGlobal } from '@warp-drive-mirror/core/types/-private';
2
+
3
+ /**
4
+ * @module
5
+ * @mergeModuleWith <project>
6
+ */
7
+
8
+ // prevents the final constructed object from needing to add
9
+ // host and namespace which are provided by the final consuming
10
+ // class to the prototype which can result in overwrite errors
11
+
12
+ const CONFIG = getOrSetGlobal('CONFIG', {
13
+ host: '',
14
+ namespace: ''
15
+ });
16
+
17
+ /**
18
+ * Sets the global configuration for `buildBaseURL`
19
+ * for host and namespace values for the application.
20
+ *
21
+ * These values may still be overridden by passing
22
+ * them to buildBaseURL directly.
23
+ *
24
+ * This method may be called as many times as needed.
25
+ * host values of `''` or `'/'` are equivalent.
26
+ *
27
+ * Except for the value of `/` as host, host should not
28
+ * end with `/`.
29
+ *
30
+ * namespace should not start or end with a `/`.
31
+ *
32
+ * ```ts
33
+ * type BuildURLConfig = {
34
+ * host: string;
35
+ * namespace: string'
36
+ * }
37
+ * ```
38
+ *
39
+ * Example:
40
+ *
41
+ * ```ts
42
+ * import { setBuildURLConfig } from '@ember-data-mirror/request-utils';
43
+ *
44
+ * setBuildURLConfig({
45
+ * host: 'https://api.example.com',
46
+ * namespace: 'api/v1'
47
+ * });
48
+ * ```
49
+ *
50
+ * @public
51
+ */
52
+ function setBuildURLConfig(config) {
53
+ CONFIG.host = config.host || '';
54
+ CONFIG.namespace = config.namespace || '';
55
+ }
56
+ const OPERATIONS_WITH_PRIMARY_RECORDS = new Set(['findRecord', 'findRelatedRecord', 'findRelatedCollection', 'updateRecord', 'deleteRecord']);
57
+ function isOperationWithPrimaryRecord(options) {
58
+ return 'op' in options && OPERATIONS_WITH_PRIMARY_RECORDS.has(options.op);
59
+ }
60
+ function resourcePathForType(options) {
61
+ return options.op === 'findMany' ? options.identifiers[0].type : options.identifier.type;
62
+ }
63
+
64
+ /**
65
+ * Builds a URL for a request based on the provided options.
66
+ * Does not include support for building query params (see `buildQueryParams`)
67
+ * so that it may be composed cleanly with other query-params strategies.
68
+ *
69
+ * Usage:
70
+ *
71
+ * ```ts
72
+ * import { buildBaseURL } from '@ember-data-mirror/request-utils';
73
+ *
74
+ * const url = buildBaseURL({
75
+ * host: 'https://api.example.com',
76
+ * namespace: 'api/v1',
77
+ * resourcePath: 'emberDevelopers',
78
+ * op: 'query',
79
+ * identifier: { type: 'ember-developer' }
80
+ * });
81
+ *
82
+ * // => 'https://api.example.com/api/v1/emberDevelopers'
83
+ * ```
84
+ *
85
+ * On the surface this may seem like a lot of work to do something simple, but
86
+ * it is designed to be composable with other utilities and interfaces that the
87
+ * average product engineer will never need to see or use.
88
+ *
89
+ * A few notes:
90
+ *
91
+ * - `resourcePath` is optional, but if it is not provided, `identifier.type` will be used.
92
+ * - `host` and `namespace` are optional, but if they are not provided, the values globally
93
+ * configured via `setBuildURLConfig` will be used.
94
+ * - `op` is required and must be one of the following:
95
+ * - 'findRecord' 'query' 'findMany' 'findRelatedCollection' 'findRelatedRecord'` 'createRecord' 'updateRecord' 'deleteRecord'
96
+ * - Depending on the value of `op`, `identifier` or `identifiers` will be required.
97
+ *
98
+ * @public
99
+ */
100
+ function buildBaseURL(urlOptions) {
101
+ const options = Object.assign({
102
+ host: CONFIG.host,
103
+ namespace: CONFIG.namespace
104
+ }, urlOptions);
105
+
106
+ // prettier-ignore
107
+ const idPath = isOperationWithPrimaryRecord(options) ? encodeURIComponent(options.identifier.id) : '';
108
+ const resourcePath = options.resourcePath || resourcePathForType(options);
109
+ const {
110
+ host,
111
+ namespace
112
+ } = options;
113
+ const fieldPath = 'fieldPath' in options ? options.fieldPath : '';
114
+ const hasHost = host !== '' && host !== '/';
115
+ const url = [hasHost ? host : '', namespace, resourcePath, idPath, fieldPath].filter(Boolean).join('/');
116
+ return hasHost ? url : `/${url}`;
117
+ }
118
+ const DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS = {
119
+ arrayFormat: 'comma'
120
+ };
121
+ function handleInclude(include) {
122
+ return typeof include === 'string' ? include.split(',') : include;
123
+ }
124
+
125
+ /**
126
+ * filter out keys of an object that have falsy values or point to empty arrays
127
+ * returning a new object with only those keys that have truthy values / non-empty arrays
128
+ *
129
+ * @public
130
+ * @param source object to filter keys with empty values from
131
+ * @return A new object with the keys that contained empty values removed
132
+ */
133
+ function filterEmpty(source) {
134
+ const result = {};
135
+ for (const key in source) {
136
+ const value = source[key];
137
+ // Allow `0` and `false` but filter falsy values that indicate "empty"
138
+ if (value !== undefined && value !== null && value !== '') {
139
+ if (!Array.isArray(value) || value.length > 0) {
140
+ result[key] = source[key];
141
+ }
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Sorts query params by both key and value returning a new URLSearchParams
149
+ * object with the keys inserted in sorted order.
150
+ *
151
+ * Treats `included` specially, splicing it into an array if it is a string and sorting the array.
152
+ *
153
+ * Options:
154
+ * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
155
+ *
156
+ * 'bracket': appends [] to the key for every value e.g. `&ids[]=1&ids[]=2`
157
+ * 'indices': appends [i] to the key for every value e.g. `&ids[0]=1&ids[1]=2`
158
+ * 'repeat': appends the key for every value e.g. `&ids=1&ids=2`
159
+ * 'comma' (default): appends the key once with a comma separated list of values e.g. `&ids=1,2`
160
+ *
161
+ * @public
162
+ * @return A {@link URLSearchParams} with keys inserted in sorted order
163
+ */
164
+ function sortQueryParams(params, options) {
165
+ const opts = Object.assign({}, DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS, options);
166
+ const paramsIsObject = !(params instanceof URLSearchParams);
167
+ const urlParams = new URLSearchParams();
168
+ const dictionaryParams = paramsIsObject ? params : {};
169
+ if (!paramsIsObject) {
170
+ params.forEach((value, key) => {
171
+ const hasExisting = key in dictionaryParams;
172
+ if (!hasExisting) {
173
+ dictionaryParams[key] = value;
174
+ } else {
175
+ const existingValue = dictionaryParams[key];
176
+ if (Array.isArray(existingValue)) {
177
+ existingValue.push(value);
178
+ } else {
179
+ dictionaryParams[key] = [existingValue, value];
180
+ }
181
+ }
182
+ });
183
+ }
184
+ if ('include' in dictionaryParams) {
185
+ dictionaryParams.include = handleInclude(dictionaryParams.include);
186
+ }
187
+ const sortedKeys = Object.keys(dictionaryParams).sort();
188
+ sortedKeys.forEach(key => {
189
+ const value = dictionaryParams[key];
190
+ if (Array.isArray(value)) {
191
+ value.sort();
192
+ switch (opts.arrayFormat) {
193
+ case 'indices':
194
+ value.forEach((v, i) => {
195
+ urlParams.append(`${key}[${i}]`, String(v));
196
+ });
197
+ return;
198
+ case 'bracket':
199
+ value.forEach(v => {
200
+ urlParams.append(`${key}[]`, String(v));
201
+ });
202
+ return;
203
+ case 'repeat':
204
+ value.forEach(v => {
205
+ urlParams.append(key, String(v));
206
+ });
207
+ return;
208
+ case 'comma':
209
+ default:
210
+ urlParams.append(key, value.join(','));
211
+ return;
212
+ }
213
+ } else {
214
+ urlParams.append(key, String(value));
215
+ }
216
+ });
217
+ return urlParams;
218
+ }
219
+
220
+ /**
221
+ * Sorts query params by both key and value, returning a query params string
222
+ *
223
+ * Treats `included` specially, splicing it into an array if it is a string and sorting the array.
224
+ *
225
+ * Options:
226
+ * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
227
+ *
228
+ * 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2`
229
+ * 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2`
230
+ * 'repeat': appends the key for every value e.g. `ids=1&ids=2`
231
+ * 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2`
232
+ *
233
+ * @public
234
+ * @return A sorted query params string without the leading `?`
235
+ */
236
+ function buildQueryParams(params, options) {
237
+ return sortQueryParams(params, options).toString();
238
+ }
239
+ export { buildBaseURL, buildQueryParams, filterEmpty, setBuildURLConfig, sortQueryParams };