cloudflare/cloudflare-typescript

Public

mirrored fromhttps://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v5.2.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/internal/qs/stringify.ts

388lines · modecode

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