@wener/utils 1.1.4 → 1.1.5

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.
@@ -0,0 +1,116 @@
1
+ import test from 'ava';
2
+ import { expectType } from 'tsd';
3
+ import { get } from './get';
4
+ import { parseObjectPath } from './parseObjectPath';
5
+
6
+ test('get typed', (t) => {
7
+ interface TestClass {
8
+ normal: string;
9
+ nested: {
10
+ a: number;
11
+ b: {
12
+ c: boolean;
13
+ };
14
+ };
15
+ arr: number[];
16
+ nestedArr: {
17
+ sum: number;
18
+ other: null;
19
+ }[];
20
+ deep: {
21
+ arr: string[];
22
+ };
23
+ deeplvl1: {
24
+ deeplvl2: {
25
+ deeplvl3: {
26
+ deeplvl4: {
27
+ value: RegExp;
28
+ };
29
+ }[];
30
+ };
31
+ }[];
32
+ }
33
+
34
+ const obj = {} as TestClass;
35
+
36
+ expectType<number>(get(obj, 'nested.a', null));
37
+ expectType<string>(get(obj, 'normal', null));
38
+ expectType<number>(get(obj, 'nested.a'));
39
+ expectType<boolean>(get(obj, 'nested.b.c'));
40
+ expectType<number[]>(get(obj, 'arr'));
41
+ expectType<number>(get(obj, 'arr[13]'));
42
+ expectType<number>(get(obj, 'arr.13'));
43
+ expectType<null>(get(obj, 'nestedArr[3].other'));
44
+ expectType<string[]>(get(obj, 'deep.deep'));
45
+ expectType<string>(get(obj, 'deep.arr[333]'));
46
+ expectType<number>(get(obj, 'deep.arr[333].length'));
47
+ expectType<boolean>(get(obj, 'nested["b"]["c"]'));
48
+ expectType<never>(get(obj, ''));
49
+ expectType<number>(get(obj, '', 3));
50
+ expectType<never>(get(obj, 'nested.asdfasdf'));
51
+ expectType<RegExp>(get(obj, 'deeplvl1[1].deeplvl2.deeplvl3[88].deeplvl4.value'));
52
+ expectType<never>(get(obj, 'deeplvl1[1].deeplvl2.deeplvl1[88].deeplvl4.value'));
53
+ expectType<TestClass>(get(obj, 'deeplvl1[1].deeplvl2.deeplvl1[88].deeplvl4.value', obj));
54
+ expectType<string>(get(obj, 'nested["dd"]', ''));
55
+ t.pass();
56
+ });
57
+
58
+ test('get', (t) => {
59
+ let obj = {
60
+ undef: undefined,
61
+ zero: 0,
62
+ one: 1,
63
+ n: null,
64
+ f: false,
65
+ a: {
66
+ two: 2,
67
+ b: {
68
+ three: 3,
69
+ c: {
70
+ four: 4,
71
+ },
72
+ },
73
+ },
74
+ arr: [5, [6, { v: 7 }]],
75
+ } as const;
76
+
77
+ function check(path: string, value: any, def?: any) {
78
+ const out = get(obj, path, def);
79
+ t.is(out, value, 'get(obj, "' + path + '") should be ' + value + ', got ' + out);
80
+ if (path) {
81
+ const arr = parseObjectPath(path);
82
+ t.is(get(obj, arr, def), value, `get(obj,${JSON.stringify(arr)}, ${def})`);
83
+ }
84
+ }
85
+
86
+ check('', undefined);
87
+ check('one', obj.one);
88
+ check('one.two', undefined);
89
+ check('a', obj.a);
90
+ check('a.two', obj.a.two);
91
+ check('a.b', obj.a.b);
92
+ check('a.b.three', obj.a.b.three);
93
+ check('a.b.c', obj.a.b.c);
94
+ check('a.b.c.four', obj.a.b.c.four);
95
+ check('n', obj.n);
96
+ check('n.badkey', undefined);
97
+ check('f', false);
98
+ check('f.badkey', undefined);
99
+
100
+ check('', 'foo', 'foo');
101
+ check('undef', 'foo', 'foo');
102
+ check('n', null, 'foo');
103
+ check('n.badkey', 'foo', 'foo');
104
+ check('zero', 0, 'foo');
105
+ check('a.badkey', 'foo', 'foo');
106
+ check('a.badkey.anotherbadkey', 'foo', 'foo');
107
+ check('f', false, 'foo');
108
+ check('f.badkey', 'foo', 'foo');
109
+
110
+ check('arr.0', 5);
111
+ check('arr.1.0', 6);
112
+ check('arr.1.1.v', 7);
113
+ check('arr[0]', 5);
114
+ check('arr[1][0]', 6);
115
+ check('arr[1][1][v]', 7);
116
+ });
@@ -0,0 +1,49 @@
1
+ import { ObjectKey, parseObjectPath } from './parseObjectPath';
2
+
3
+ /**
4
+ * get by path
5
+ *
6
+ * {@link https://github.com/developit/dlv dlv}
7
+ */
8
+ export function get<O extends object, P extends ObjectKey, OrElse extends unknown>(
9
+ obj: O,
10
+ key: P | P[],
11
+ def?: OrElse,
12
+ ): ResolveObjectPathType<O, P, OrElse> {
13
+ const undef = undefined;
14
+ const path = parseObjectPath(key);
15
+ let out: any = obj;
16
+ for (const i of path) {
17
+ out = out ? out[i] : undef;
18
+ }
19
+ return out === undef ? def : out;
20
+ }
21
+
22
+ /**
23
+ * It tries to resolve the path of the given object, otherwise it will return OrElse
24
+ *
25
+ * {@link https://github.com/Pouja/typescript-deep-path-safe typescript-deep-path-safe}
26
+ */
27
+ export type ResolveObjectPathType<
28
+ ObjectType,
29
+ Path extends string | symbol | number,
30
+ OrElse,
31
+ > = Path extends keyof ObjectType
32
+ ? ObjectType[Path]
33
+ : Path extends `${infer LeftSide}.${infer RightSide}`
34
+ ? LeftSide extends keyof ObjectType
35
+ ? ResolveObjectPathType<ObjectType[LeftSide], RightSide, OrElse>
36
+ : Path extends `${infer LeftSide}[${number}].${infer RightSide}`
37
+ ? LeftSide extends keyof ObjectType
38
+ ? ObjectType[LeftSide] extends Array<infer U>
39
+ ? ResolveObjectPathType<U, RightSide, OrElse>
40
+ : OrElse
41
+ : OrElse
42
+ : OrElse
43
+ : Path extends `${infer LeftSide}[${number}]`
44
+ ? LeftSide extends keyof ObjectType
45
+ ? ObjectType[LeftSide] extends Array<infer U>
46
+ ? U
47
+ : OrElse
48
+ : OrElse
49
+ : OrElse;
@@ -0,0 +1,18 @@
1
+ import test from 'ava';
2
+ import { parseObjectPath } from './parseObjectPath';
3
+
4
+ test('parseObjectPath', (t) => {
5
+ for (const [k, v] of [
6
+ ['a.b.c', ['a', 'b', 'c']],
7
+ ['a[1].c', ['a', '1', 'c']],
8
+ ['a.1.c', ['a', '1', 'c']],
9
+ ['a[b].c', ['a', 'b', 'c']],
10
+ ['arr[1][0]', ['arr', '1', '0']],
11
+ ['arr.2', ['arr', '2']],
12
+ ['arr[2]', ['arr', '2']],
13
+ [Symbol.toStringTag, [Symbol.toStringTag]],
14
+ [[Symbol.toStringTag], [Symbol.toStringTag]],
15
+ ]) {
16
+ t.deepEqual(parseObjectPath(k), v);
17
+ }
18
+ });
@@ -0,0 +1,40 @@
1
+ export type ObjectKey = string | symbol | number;
2
+ export type ObjectPath = Array<ObjectKey>;
3
+ export type ObjectPathLike = ObjectKey | ObjectPath;
4
+
5
+ /**
6
+ * Parse object path
7
+ *
8
+ * parseObjectPath('a.b.c') // => ['a', 'b', 'c']
9
+ * parseObjectPath('a[0].b') // => ['a', 0, 'b']
10
+ * parseObjectPath('a[0][1]') // => ['a', 0, 1]
11
+ *
12
+ */
13
+ export function parseObjectPath(s: ObjectPathLike): ObjectPath {
14
+ if (typeof s !== 'string') {
15
+ return Array.isArray(s) ? s : [s];
16
+ }
17
+ const parts = s.split('.');
18
+ // fast path
19
+ if (!s.includes('[')) {
20
+ return parts;
21
+ }
22
+
23
+ const result = [];
24
+ for (const part of parts) {
25
+ if (!part.endsWith(']')) {
26
+ result.push(part);
27
+ } else {
28
+ // a[0][1]
29
+ // try parseInt can extend to support a[-1] to use .at access
30
+ const s = part.split('[');
31
+ for (let sub of s) {
32
+ if (sub.endsWith(']')) {
33
+ sub = sub.slice(0, -1);
34
+ }
35
+ result.push(sub);
36
+ }
37
+ }
38
+ }
39
+ return result;
40
+ }
@@ -0,0 +1,405 @@
1
+ import test from 'ava';
2
+ import { set } from './set';
3
+
4
+ test('set basics', (t) => {
5
+ t.is(set({}, 'c', 3), undefined, 'should not give return value');
6
+ {
7
+ let item = { foo: 1 };
8
+ set(item, 'bar', 123);
9
+ t.is(item, item);
10
+ t.deepEqual(
11
+ item,
12
+ {
13
+ foo: 1,
14
+ bar: 123,
15
+ },
16
+ 'should mutate original object',
17
+ );
18
+ }
19
+ });
20
+
21
+ test('set objects', (t) => {
22
+ const prepare = (x: any) => ({ input: x, copy: JSON.parse(JSON.stringify(x)) });
23
+ const objects = (s: string, f: Function) => {
24
+ t.log(s);
25
+ f();
26
+ };
27
+ const orig = set;
28
+
29
+ function run(isMerge: boolean) {
30
+ const set = (a: any, b: any, c: any) => orig(a, b, c, isMerge);
31
+ const verb = isMerge ? 'merge' : 'overwrite';
32
+ objects(`should ${verb} existing object value :: simple`, () => {
33
+ let { input } = prepare({
34
+ hello: { a: 1 },
35
+ });
36
+
37
+ set(input, 'hello', { foo: 123 });
38
+
39
+ if (isMerge) {
40
+ t.deepEqual(input, {
41
+ hello: {
42
+ a: 1,
43
+ foo: 123,
44
+ },
45
+ });
46
+ } else {
47
+ t.deepEqual(input, {
48
+ hello: { foo: 123 },
49
+ });
50
+ }
51
+ });
52
+
53
+ objects(`should ${verb} existing object value :: nested`, () => {
54
+ let { input, copy } = prepare({
55
+ a: {
56
+ b: {
57
+ c: 123,
58
+ },
59
+ },
60
+ });
61
+
62
+ set(input, 'a.b', { foo: 123 });
63
+
64
+ if (isMerge) {
65
+ Object.assign(copy.a.b, { foo: 123 });
66
+ } else {
67
+ copy.a.b = { foo: 123 };
68
+ }
69
+
70
+ t.deepEqual(input, copy);
71
+ });
72
+
73
+ objects(`should ${verb} existing array value :: simple`, () => {
74
+ let { input } = prepare([{ foo: 1 }]);
75
+
76
+ set(input, '0', { bar: 2 });
77
+
78
+ if (isMerge) {
79
+ t.deepEqual(input, [{ foo: 1, bar: 2 }]);
80
+ } else {
81
+ t.deepEqual(input, [{ bar: 2 }]);
82
+ }
83
+ });
84
+
85
+ objects(`should ${verb} existing array value :: nested`, () => {
86
+ let { input } = prepare([
87
+ { name: 'bob', age: 56, friends: ['foobar'] },
88
+ { name: 'alice', age: 47, friends: ['mary'] },
89
+ ]);
90
+
91
+ set(input, '0', { age: 57, friends: ['alice', 'mary'] });
92
+ set(input, '1', { friends: ['bob'] });
93
+ set(input, '2', { name: 'mary', age: 49, friends: ['bob'] });
94
+
95
+ if (isMerge) {
96
+ t.deepEqual(input, [
97
+ { name: 'bob', age: 57, friends: ['alice', 'mary'] },
98
+ { name: 'alice', age: 47, friends: ['bob'] },
99
+ { name: 'mary', age: 49, friends: ['bob'] },
100
+ ]);
101
+ } else {
102
+ t.deepEqual(input, [
103
+ { age: 57, friends: ['alice', 'mary'] },
104
+ { friends: ['bob'] },
105
+ { name: 'mary', age: 49, friends: ['bob'] },
106
+ ]);
107
+ }
108
+ });
109
+ }
110
+
111
+ run(true);
112
+ run(false);
113
+ });
114
+
115
+ test('set arrays', (t) => {
116
+ const arrays = (s: string, f: Function) => {
117
+ t.log(s);
118
+ f();
119
+ };
120
+ arrays('should create array instead of object via numeric key :: simple', () => {
121
+ let input: any = { a: 1 };
122
+ set(input, 'e.0', 2);
123
+ t.true(Array.isArray(input.e));
124
+ t.is(input.e[0], 2);
125
+ t.deepEqual(input, {
126
+ a: 1,
127
+ e: [2],
128
+ });
129
+ });
130
+
131
+ arrays('should create array instead of object via numeric key :: nested', () => {
132
+ let input: any = { a: 1 };
133
+ set(input, 'e.0.0', 123);
134
+ t.true(input.e instanceof Array);
135
+ t.is(input.e[0][0], 123);
136
+ t.deepEqual(input, {
137
+ a: 1,
138
+ e: [[123]],
139
+ });
140
+ });
141
+
142
+ arrays('should be able to create object inside of array', () => {
143
+ let input: any = {};
144
+ set(input, ['x', '0', 'z'], 123);
145
+ t.true(input.x instanceof Array);
146
+ t.deepEqual(input, {
147
+ x: [{ z: 123 }],
148
+ });
149
+ });
150
+
151
+ arrays('should create arrays with hole(s) if needed', () => {
152
+ let input: any = {};
153
+ set(input, ['x', '1', 'z'], 123);
154
+ t.true(input.x instanceof Array);
155
+ t.deepEqual(input, {
156
+ x: [, { z: 123 }],
157
+ });
158
+ });
159
+
160
+ arrays('should create object from decimal-like key :: array :: zero :: string', () => {
161
+ let input: any = {};
162
+ set(input, ['x', '10.0', 'z'], 123);
163
+ t.false(input.x instanceof Array);
164
+ t.deepEqual(input, {
165
+ x: {
166
+ '10.0': {
167
+ z: 123,
168
+ },
169
+ },
170
+ });
171
+ });
172
+
173
+ arrays('should create array from decimal-like key :: array :: zero :: number', () => {
174
+ let input: any = {};
175
+ set(input, ['x', 10.0, 'z'], 123);
176
+ t.true(input.x instanceof Array);
177
+
178
+ let x = Array(10);
179
+ x.push({ z: 123 });
180
+ t.deepEqual(input, { x });
181
+ });
182
+
183
+ arrays('should create object from decimal-like key :: array :: nonzero', () => {
184
+ let input: any = {};
185
+ set(input, ['x', '10.2', 'z'], 123);
186
+ t.false(input.x instanceof Array);
187
+ t.deepEqual(input, {
188
+ x: {
189
+ '10.2': {
190
+ z: 123,
191
+ },
192
+ },
193
+ });
194
+ });
195
+ });
196
+
197
+ test('set pollution', (t) => {
198
+ const pollution = (s: string, f: Function) => {
199
+ t.log(s);
200
+ f();
201
+ };
202
+ pollution('should protect against "__proto__" assignment', () => {
203
+ let input: any = { abc: 123 };
204
+ let before = input.__proto__;
205
+ set(input, '__proto__.hello', 123);
206
+
207
+ t.deepEqual(input.__proto__, before);
208
+ t.deepEqual(input, {
209
+ abc: 123,
210
+ });
211
+ });
212
+
213
+ pollution('should protect against "__proto__" assignment :: nested', () => {
214
+ let input: any = { abc: 123 };
215
+ let before = input.__proto__;
216
+ set(input, ['xyz', '__proto__', 'hello'], 123);
217
+
218
+ t.deepEqual(input.__proto__, before);
219
+ t.deepEqual(input, {
220
+ abc: 123,
221
+ xyz: {
222
+ // empty
223
+ },
224
+ });
225
+
226
+ t.is(input.hello, undefined);
227
+ });
228
+
229
+ pollution('should ignore "prototype" assignment', () => {
230
+ let input: any = { a: 123 };
231
+ set(input, 'a.prototype.hello', 'world');
232
+
233
+ t.is(input.a.prototype, undefined);
234
+ t.is(input.a.hello, undefined);
235
+
236
+ t.deepEqual(input, {
237
+ a: {
238
+ // converted, then aborted
239
+ },
240
+ });
241
+
242
+ t.is(JSON.stringify(input), '{"a":{}}');
243
+ });
244
+
245
+ pollution('should ignore "constructor" assignment :: direct', () => {
246
+ let input: any = { a: 123 };
247
+
248
+ function Custom() {
249
+ //
250
+ }
251
+
252
+ set(input, 'a.constructor', Custom);
253
+ t.not(input.a.constructor, Custom);
254
+ t.false(input.a instanceof Custom);
255
+
256
+ t.true(input.a.constructor instanceof Object, '~> 123 -> {}');
257
+ t.is(input.a.hasOwnProperty('constructor'), false);
258
+ t.deepEqual(input, { a: {} });
259
+ });
260
+
261
+ pollution('should ignore "constructor" assignment :: nested', () => {
262
+ let input: any = {};
263
+
264
+ set(input, 'constructor.prototype.hello', 'world');
265
+ t.is(input.hasOwnProperty('constructor'), false);
266
+ t.is(input.hasOwnProperty('hello'), false);
267
+
268
+ t.deepEqual(input, {
269
+ // empty
270
+ });
271
+ });
272
+
273
+ // Test for CVE-2022-25645 - CWE-1321
274
+ pollution('should ignore JSON.parse crafted object with "__proto__" key', () => {
275
+ let a: any = { b: { c: 1 } };
276
+ t.is(a.polluted, undefined);
277
+ set(a, 'b', JSON.parse('{"__proto__":{"polluted":"Yes!"}}'));
278
+ t.is(a.polluted, undefined);
279
+ });
280
+ });
281
+ test('set assigns', (t) => {
282
+ const assigns = (s: string, f: Function) => {
283
+ t.log(s);
284
+ f();
285
+ };
286
+ assigns('should add value to key path :: shallow :: string', () => {
287
+ let input: any = {};
288
+ set(input, 'abc', 123);
289
+ t.deepEqual(input, { abc: 123 });
290
+ });
291
+
292
+ assigns('should add value to key path :: shallow :: array', () => {
293
+ let input: any = {};
294
+ set(input, ['abc'], 123);
295
+ t.deepEqual(input, { abc: 123 });
296
+ });
297
+
298
+ assigns('should add value to key path :: nested :: string', () => {
299
+ let input: any = {};
300
+ set(input, 'a.b.c', 123);
301
+ t.deepEqual(input, {
302
+ a: {
303
+ b: {
304
+ c: 123,
305
+ },
306
+ },
307
+ });
308
+ });
309
+
310
+ assigns('should add value to key path :: nested :: array', () => {
311
+ let input: any = {};
312
+ set(input, ['a', 'b', 'c'], 123);
313
+ t.deepEqual(input, {
314
+ a: {
315
+ b: {
316
+ c: 123,
317
+ },
318
+ },
319
+ });
320
+ });
321
+
322
+ assigns('should create Array via integer key :: string', () => {
323
+ let input: any = {};
324
+ set(input, ['foo', '0'], 123);
325
+ t.true(input.foo instanceof Array);
326
+ t.deepEqual(input, {
327
+ foo: [123],
328
+ });
329
+ });
330
+
331
+ assigns('should create Array via integer key :: number', () => {
332
+ let input: any = {};
333
+ set(input, ['foo', 0], 123);
334
+ t.true(input.foo instanceof Array);
335
+ t.deepEqual(input, {
336
+ foo: [123],
337
+ });
338
+ });
339
+ });
340
+ test('set preserves', (t) => {
341
+ const preserves = (s: string, f: Function) => {
342
+ t.log(s);
343
+ f();
344
+ };
345
+ preserves('should preserve existing object structure', () => {
346
+ let input = {
347
+ a: {
348
+ b: {
349
+ c: 123,
350
+ },
351
+ },
352
+ };
353
+
354
+ set(input, 'a.b.x.y', 456);
355
+
356
+ t.deepEqual(input, {
357
+ a: {
358
+ b: {
359
+ c: 123,
360
+ x: {
361
+ y: 456,
362
+ },
363
+ },
364
+ },
365
+ });
366
+ });
367
+
368
+ preserves('should overwrite existing non-object values as object', () => {
369
+ let input = {
370
+ a: {
371
+ b: 123,
372
+ },
373
+ };
374
+
375
+ set(input, 'a.b.c', 'hello');
376
+
377
+ t.deepEqual(input, {
378
+ a: {
379
+ b: {
380
+ c: 'hello',
381
+ },
382
+ },
383
+ });
384
+ });
385
+
386
+ preserves('should preserve existing object tree w/ array value', () => {
387
+ let input = {
388
+ a: {
389
+ b: {
390
+ c: 123,
391
+ d: {
392
+ e: 5,
393
+ },
394
+ },
395
+ },
396
+ };
397
+
398
+ set(input, 'a.b.d.z', [1, 2, 3, 4]);
399
+
400
+ t.deepEqual(input.a.b.d, {
401
+ e: 5,
402
+ z: [1, 2, 3, 4],
403
+ });
404
+ });
405
+ });
@@ -0,0 +1,48 @@
1
+ import { ObjectKey, ObjectPath, parseObjectPath } from './parseObjectPath';
2
+
3
+ /**
4
+ * Deep set
5
+ *
6
+ * {@link https://github.com/lukeed/dset dset}
7
+ */
8
+ export function set<T extends object, V>(obj: T, key: ObjectKey | ObjectPath, val: V, merging = true) {
9
+ const path = parseObjectPath(key);
10
+ let i = 0;
11
+ const len = path.length;
12
+ let current: any = obj;
13
+ let x, k;
14
+ while (i < len) {
15
+ k = path[i++];
16
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') break;
17
+ // noinspection PointlessArithmeticExpressionJS
18
+ current = current[k] =
19
+ i === len
20
+ ? merging
21
+ ? merge(current[k], val)
22
+ : val
23
+ : typeof (x = current[k]) === typeof path
24
+ ? x
25
+ : // @ts-ignore hacky type check
26
+ path[i] * 0 !== 0 || !!~('' + path[i]).indexOf('.')
27
+ ? {}
28
+ : [];
29
+ }
30
+ }
31
+
32
+ export function merge(a: any, b: any) {
33
+ let k;
34
+ if (typeof a === 'object' && typeof b === 'object') {
35
+ if (Array.isArray(a) && Array.isArray(b)) {
36
+ for (k = 0; k < b.length; k++) {
37
+ a[k] = merge(a[k], b[k]);
38
+ }
39
+ } else {
40
+ for (k in b) {
41
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') break;
42
+ a[k] = merge(a[k], b[k]);
43
+ }
44
+ }
45
+ return a;
46
+ }
47
+ return b;
48
+ }
@@ -0,0 +1,25 @@
1
+ import test from 'ava';
2
+ import { renderTemplate } from './renderTemplate';
3
+
4
+ test('renderTemplate', (t) => {
5
+ const obj = {
6
+ name: 'wener',
7
+ authors: [
8
+ {
9
+ name: 'wener',
10
+ },
11
+ ],
12
+ };
13
+ for (const [k, v] of Object.entries({
14
+ 'My name is ${name}': 'My name is wener',
15
+ 'My name is ${ authors[0].name }': 'My name is wener',
16
+ })) {
17
+ t.is(renderTemplate(k, obj), v);
18
+ }
19
+ t.is(
20
+ renderTemplate('My name is ${name}', (v) => v),
21
+ 'My name is name',
22
+ );
23
+ t.is(renderTemplate('My name is ${name}', obj, 'common'), 'My name is ${name}');
24
+ t.is(renderTemplate('My name is {{name}}', obj, 'common'), 'My name is wener');
25
+ });