cloudflare/cloudflare-typescript

Public

mirrored from https://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cbd18e5b426c5abe0a1e3e1ad6d0da11eb2e5439

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/internal/qs/stringify.ts

385lines · modecode

1import { encode, is_buffer, maybe_map, has } from './utils';
2import { default_format, default_formatter, formatters } from './formats';
3import type { NonNullableProperties, StringifyOptions } from './types';
4import { isArray } from '../utils/values';
5
6const array_prefix_generators = {
7 brackets(prefix: PropertyKey) {
8 return String(prefix) + '[]';
9 },
10 comma: 'comma',
11 indices(prefix: PropertyKey, key: string) {
12 return String(prefix) + '[' + key + ']';
13 },
14 repeat(prefix: PropertyKey) {
15 return String(prefix);
16 },
17};
18
19const push_to_array = function (arr: any[], value_or_array: any) {
20 Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]);
21};
22
23let toISOString;
24
25const defaults = {
26 addQueryPrefix: false,
27 allowDots: false,
28 allowEmptyArrays: false,
29 arrayFormat: 'indices',
30 charset: 'utf-8',
31 charsetSentinel: false,
32 delimiter: '&',
33 encode: true,
34 encodeDotInKeys: false,
35 encoder: encode,
36 encodeValuesOnly: false,
37 format: default_format,
38 formatter: default_formatter,
39 /** @deprecated */
40 indices: false,
41 serializeDate(date) {
42 return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date);
43 },
44 skipNulls: false,
45 strictNullHandling: false,
46} as NonNullableProperties<StringifyOptions & { formatter: (typeof formatters)['RFC1738'] }>;
47
48function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint {
49 return (
50 typeof v === 'string' ||
51 typeof v === 'number' ||
52 typeof v === 'boolean' ||
53 typeof v === 'symbol' ||
54 typeof v === 'bigint'
55 );
56}
57
58const sentinel = {};
59
60function inner_stringify(
61 object: any,
62 prefix: PropertyKey,
63 generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string),
64 commaRoundTrip: boolean,
65 allowEmptyArrays: boolean,
66 strictNullHandling: boolean,
67 skipNulls: boolean,
68 encodeDotInKeys: boolean,
69 encoder: StringifyOptions['encoder'],
70 filter: StringifyOptions['filter'],
71 sort: StringifyOptions['sort'],
72 allowDots: StringifyOptions['allowDots'],
73 serializeDate: StringifyOptions['serializeDate'],
74 format: StringifyOptions['format'],
75 formatter: StringifyOptions['formatter'],
76 encodeValuesOnly: boolean,
77 charset: StringifyOptions['charset'],
78 sideChannel: WeakMap<any, any>,
79) {
80 let obj = object;
81
82 let tmp_sc = sideChannel;
83 let step = 0;
84 let find_flag = false;
85 while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) {
86 // Where object last appeared in the ref tree
87 const pos = tmp_sc.get(object);
88 step += 1;
89 if (typeof pos !== 'undefined') {
90 if (pos === step) {
91 throw new RangeError('Cyclic object value');
92 } else {
93 find_flag = true; // Break while
94 }
95 }
96 if (typeof tmp_sc.get(sentinel) === 'undefined') {
97 step = 0;
98 }
99 }
100
101 if (typeof filter === 'function') {
102 obj = filter(prefix, obj);
103 } else if (obj instanceof Date) {
104 obj = serializeDate?.(obj);
105 } else if (generateArrayPrefix === 'comma' && isArray(obj)) {
106 obj = maybe_map(obj, function (value) {
107 if (value instanceof Date) {
108 return serializeDate?.(value);
109 }
110 return value;
111 });
112 }
113
114 if (obj === null) {
115 if (strictNullHandling) {
116 return encoder && !encodeValuesOnly ?
117 // @ts-expect-error
118 encoder(prefix, defaults.encoder, charset, 'key', format)
119 : prefix;
120 }
121
122 obj = '';
123 }
124
125 if (is_non_nullish_primitive(obj) || is_buffer(obj)) {
126 if (encoder) {
127 const key_value =
128 encodeValuesOnly ? prefix
129 // @ts-expect-error
130 : encoder(prefix, defaults.encoder, charset, 'key', format);
131 return [
132 formatter?.(key_value) +
133 '=' +
134 // @ts-expect-error
135 formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)),
136 ];
137 }
138 return [formatter?.(prefix) + '=' + formatter?.(String(obj))];
139 }
140
141 const values: string[] = [];
142
143 if (typeof obj === 'undefined') {
144 return values;
145 }
146
147 let obj_keys;
148 if (generateArrayPrefix === 'comma' && isArray(obj)) {
149 // we need to join elements in
150 if (encodeValuesOnly && encoder) {
151 // @ts-expect-error values only
152 obj = maybe_map(obj, encoder);
153 }
154 obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
155 } else if (isArray(filter)) {
156 obj_keys = filter;
157 } else {
158 const keys = Object.keys(obj);
159 obj_keys = sort ? keys.sort(sort) : keys;
160 }
161
162 const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix);
163
164 const adjusted_prefix =
165 commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix;
166
167 if (allowEmptyArrays && isArray(obj) && obj.length === 0) {
168 return adjusted_prefix + '[]';
169 }
170
171 for (let j = 0; j < obj_keys.length; ++j) {
172 const key = obj_keys[j];
173 const value =
174 // @ts-ignore
175 typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any];
176
177 if (skipNulls && value === null) {
178 continue;
179 }
180
181 // @ts-ignore
182 const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key;
183 const key_prefix =
184 isArray(obj) ?
185 typeof generateArrayPrefix === 'function' ?
186 generateArrayPrefix(adjusted_prefix, encoded_key)
187 : adjusted_prefix
188 : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']');
189
190 sideChannel.set(object, step);
191 const valueSideChannel = new WeakMap();
192 valueSideChannel.set(sentinel, sideChannel);
193 push_to_array(
194 values,
195 inner_stringify(
196 value,
197 key_prefix,
198 generateArrayPrefix,
199 commaRoundTrip,
200 allowEmptyArrays,
201 strictNullHandling,
202 skipNulls,
203 encodeDotInKeys,
204 // @ts-ignore
205 generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder,
206 filter,
207 sort,
208 allowDots,
209 serializeDate,
210 format,
211 formatter,
212 encodeValuesOnly,
213 charset,
214 valueSideChannel,
215 ),
216 );
217 }
218
219 return values;
220}
221
222function normalize_stringify_options(
223 opts: StringifyOptions = defaults,
224): NonNullableProperties<Omit<StringifyOptions, 'indices'>> & { indices?: boolean } {
225 if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
226 throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
227 }
228
229 if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') {
230 throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided');
231 }
232
233 if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
234 throw new TypeError('Encoder has to be a function.');
235 }
236
237 const charset = opts.charset || defaults.charset;
238 if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
239 throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
240 }
241
242 let format = default_format;
243 if (typeof opts.format !== 'undefined') {
244 if (!has(formatters, opts.format)) {
245 throw new TypeError('Unknown format option provided.');
246 }
247 format = opts.format;
248 }
249 const formatter = formatters[format];
250
251 let filter = defaults.filter;
252 if (typeof opts.filter === 'function' || isArray(opts.filter)) {
253 filter = opts.filter;
254 }
255
256 let arrayFormat: StringifyOptions['arrayFormat'];
257 if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) {
258 arrayFormat = opts.arrayFormat;
259 } else if ('indices' in opts) {
260 arrayFormat = opts.indices ? 'indices' : 'repeat';
261 } else {
262 arrayFormat = defaults.arrayFormat;
263 }
264
265 if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
266 throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
267 }
268
269 const allowDots =
270 typeof opts.allowDots === 'undefined' ?
271 !!opts.encodeDotInKeys === true ?
272 true
273 : defaults.allowDots
274 : !!opts.allowDots;
275
276 return {
277 addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
278 // @ts-ignore
279 allowDots: allowDots,
280 allowEmptyArrays:
281 typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
282 arrayFormat: arrayFormat,
283 charset: charset,
284 charsetSentinel:
285 typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
286 commaRoundTrip: !!opts.commaRoundTrip,
287 delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
288 encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
289 encodeDotInKeys:
290 typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys,
291 encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
292 encodeValuesOnly:
293 typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
294 filter: filter,
295 format: format,
296 formatter: formatter,
297 serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
298 skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
299 // @ts-ignore
300 sort: typeof opts.sort === 'function' ? opts.sort : null,
301 strictNullHandling:
302 typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
303 };
304}
305
306export function stringify(object: any, opts: StringifyOptions = {}) {
307 let obj = object;
308 const options = normalize_stringify_options(opts);
309
310 let obj_keys: PropertyKey[] | undefined;
311 let filter;
312
313 if (typeof options.filter === 'function') {
314 filter = options.filter;
315 obj = filter('', obj);
316 } else if (isArray(options.filter)) {
317 filter = options.filter;
318 obj_keys = filter;
319 }
320
321 const keys: string[] = [];
322
323 if (typeof obj !== 'object' || obj === null) {
324 return '';
325 }
326
327 const generateArrayPrefix = array_prefix_generators[options.arrayFormat];
328 const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip;
329
330 if (!obj_keys) {
331 obj_keys = Object.keys(obj);
332 }
333
334 if (options.sort) {
335 obj_keys.sort(options.sort);
336 }
337
338 const sideChannel = new WeakMap();
339 for (let i = 0; i < obj_keys.length; ++i) {
340 const key = obj_keys[i]!;
341
342 if (options.skipNulls && obj[key] === null) {
343 continue;
344 }
345 push_to_array(
346 keys,
347 inner_stringify(
348 obj[key],
349 key,
350 // @ts-expect-error
351 generateArrayPrefix,
352 commaRoundTrip,
353 options.allowEmptyArrays,
354 options.strictNullHandling,
355 options.skipNulls,
356 options.encodeDotInKeys,
357 options.encode ? options.encoder : null,
358 options.filter,
359 options.sort,
360 options.allowDots,
361 options.serializeDate,
362 options.format,
363 options.formatter,
364 options.encodeValuesOnly,
365 options.charset,
366 sideChannel,
367 ),
368 );
369 }
370
371 const joined = keys.join(options.delimiter);
372 let prefix = options.addQueryPrefix === true ? '?' : '';
373
374 if (options.charsetSentinel) {
375 if (options.charset === 'iso-8859-1') {
376 // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
377 prefix += 'utf8=%26%2310003%3B&';
378 } else {
379 // encodeURIComponent('✓')
380 prefix += 'utf8=%E2%9C%93&';
381 }
382 }
383
384 return joined.length > 0 ? prefix + joined : '';
385}
386