@beecode/msh-util 1.2.0 → 1.2.1

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,192 @@
1
+ import Joi from 'joi'
2
+ import { JoiUtil } from 'src/joi-util'
3
+
4
+ describe('JoiUtil', () => {
5
+ const joiUtil = new JoiUtil()
6
+
7
+ const validObject = { a: 'string', b: true, c: 123, d: new Date() }
8
+ const invalidObject = { invalid: true }
9
+
10
+ const joiSchema = Joi.object().keys({
11
+ a: Joi.string().required(),
12
+ b: Joi.boolean().required(),
13
+ c: Joi.number().required(),
14
+ d: Joi.date().required(),
15
+ })
16
+
17
+ describe('sanitize', () => {
18
+ it('should return valid object', () => {
19
+ const result = joiUtil.sanitize(validObject, joiSchema)
20
+ expect(result).toEqual(validObject)
21
+ })
22
+ it('should return valid object and remove properties that are not defined in schema', () => {
23
+ const result = joiUtil.sanitize({ ...validObject, test: true }, joiSchema)
24
+ expect(result).toEqual(validObject)
25
+ expect(result.test).toBeUndefined()
26
+ })
27
+ it('should throw error if object is not valid, return first error', () => {
28
+ try {
29
+ joiUtil.sanitize(invalidObject, joiSchema)
30
+ expect.fail('test failed')
31
+ } catch (err: any) {
32
+ expect(err.message).toEqual("'a' is required")
33
+ expect(err.payload.details).toEqual([
34
+ {
35
+ context: {
36
+ key: 'a',
37
+ label: 'a',
38
+ },
39
+ message: '"a" is required',
40
+ path: ['a'],
41
+ type: 'any.required',
42
+ },
43
+ ])
44
+ }
45
+ })
46
+ it('should throw error if object is not valid, return all errors', () => {
47
+ try {
48
+ joiUtil.sanitize(invalidObject, joiSchema, { abortEarly: false })
49
+ expect.fail('test failed')
50
+ } catch (err: any) {
51
+ expect(err.message).toEqual("'a' is required. 'b' is required. 'c' is required. 'd' is required")
52
+ expect(err.payload.details).toEqual([
53
+ {
54
+ context: {
55
+ key: 'a',
56
+ label: 'a',
57
+ },
58
+ message: '"a" is required',
59
+ path: ['a'],
60
+ type: 'any.required',
61
+ },
62
+ {
63
+ context: {
64
+ key: 'b',
65
+ label: 'b',
66
+ },
67
+ message: '"b" is required',
68
+ path: ['b'],
69
+ type: 'any.required',
70
+ },
71
+ {
72
+ context: {
73
+ key: 'c',
74
+ label: 'c',
75
+ },
76
+ message: '"c" is required',
77
+ path: ['c'],
78
+ type: 'any.required',
79
+ },
80
+ {
81
+ context: {
82
+ key: 'd',
83
+ label: 'd',
84
+ },
85
+ message: '"d" is required',
86
+ path: ['d'],
87
+ type: 'any.required',
88
+ },
89
+ ])
90
+ }
91
+ })
92
+ })
93
+
94
+ describe('validate', () => {
95
+ it('should return valid object', () => {
96
+ const result = joiUtil.validate(validObject, joiSchema)
97
+ expect(result).toEqual(validObject)
98
+ })
99
+
100
+ it('should throw error if there are unknown properties', () => {
101
+ try {
102
+ joiUtil.validate({ ...validObject, test: true }, joiSchema)
103
+ expect.fail('test failed')
104
+ } catch (err: any) {
105
+ expect(err.message).toEqual("'test' is not allowed")
106
+ }
107
+ })
108
+ it('should throw error if object is not valid, return first error', () => {
109
+ try {
110
+ joiUtil.validate(invalidObject, joiSchema)
111
+ expect.fail('test failed')
112
+ } catch (err: any) {
113
+ expect(err.message).toEqual("'a' is required")
114
+ expect(err.payload.details).toEqual([
115
+ {
116
+ context: {
117
+ key: 'a',
118
+ label: 'a',
119
+ },
120
+ message: '"a" is required',
121
+ path: ['a'],
122
+ type: 'any.required',
123
+ },
124
+ ])
125
+ }
126
+ })
127
+ it('should throw error if object is not valid, return all errors', () => {
128
+ try {
129
+ joiUtil.validate(invalidObject, joiSchema, { abortEarly: false })
130
+ expect.fail('test failed')
131
+ } catch (err: any) {
132
+ expect(err.message).toEqual(
133
+ "'a' is required. 'b' is required. 'c' is required. 'd' is required. 'invalid' is not allowed"
134
+ )
135
+ expect(err.payload.details).toEqual([
136
+ {
137
+ context: {
138
+ key: 'a',
139
+ label: 'a',
140
+ },
141
+ message: '"a" is required',
142
+ path: ['a'],
143
+ type: 'any.required',
144
+ },
145
+ {
146
+ context: {
147
+ key: 'b',
148
+ label: 'b',
149
+ },
150
+ message: '"b" is required',
151
+ path: ['b'],
152
+ type: 'any.required',
153
+ },
154
+ {
155
+ context: {
156
+ key: 'c',
157
+ label: 'c',
158
+ },
159
+ message: '"c" is required',
160
+ path: ['c'],
161
+ type: 'any.required',
162
+ },
163
+ {
164
+ context: {
165
+ key: 'd',
166
+ label: 'd',
167
+ },
168
+ message: '"d" is required',
169
+ path: ['d'],
170
+ type: 'any.required',
171
+ },
172
+ {
173
+ context: {
174
+ child: 'invalid',
175
+ key: 'invalid',
176
+ label: 'invalid',
177
+ value: true,
178
+ },
179
+ message: '"invalid" is not allowed',
180
+ path: ['invalid'],
181
+ type: 'object.unknown',
182
+ },
183
+ ])
184
+ }
185
+ })
186
+
187
+ it('should allow unknown if flag is set and call logger with warn message', () => {
188
+ const result = joiUtil.validate({ ...validObject, unknownProp: 'test' }, joiSchema, { allowUnknown: true })
189
+ expect(result).toEqual({ ...validObject, unknownProp: 'test' })
190
+ })
191
+ })
192
+ })
@@ -0,0 +1,65 @@
1
+ import { ObjectSchema, Schema, ValidationOptions } from 'joi'
2
+
3
+ export class ErrorWithPayload<T> extends Error {
4
+ payload: T
5
+
6
+ constructor(message: string, payload: T) {
7
+ super(message)
8
+ this.payload = payload
9
+ }
10
+ }
11
+
12
+ /**
13
+ * This is a simple wrapper around Joi validation with two functions exposed validate and sanitize. If object is not valid function throws an error.
14
+ * @example
15
+ * type SomeType = {
16
+ * a: string
17
+ * b: number
18
+ * }
19
+ * const someSchema = Joi.object<SomeType>().keys({
20
+ * a: Joi.string().required(),
21
+ * b: Joi.number().required(),
22
+ * }).required()
23
+ *
24
+ * const joiUtil = new JoiUtil()
25
+ *
26
+ * // using
27
+ * const invalidObject = joiUtil.validate({}, someSchema) // validate throws an error
28
+ * const validObject = joiUtil.validate({ a: 'a', b: 1 }, someSchema)
29
+ */
30
+ export class JoiUtil {
31
+ /**
32
+ * Validate and clean object
33
+ * @template T
34
+ * @template Joi
35
+ * @param {any} objectToValidate
36
+ * @param {Joi.Schema | Joi.ObjectSchema<T>} schema
37
+ * @param {validationOptions} [validationOptions]
38
+ * @returns {T} expected object after validation
39
+ */
40
+ sanitize<T>(objectToValidate: any, schema: Schema | ObjectSchema<T>, validationOptions?: ValidationOptions): T {
41
+ return this._validate<T>(objectToValidate, schema, { ...validationOptions, stripUnknown: true })
42
+ }
43
+
44
+ /**
45
+ * Only validate properties specified in validation schema
46
+ * @template T
47
+ * @template Joi
48
+ * @param {any} objectToValidate
49
+ * @param {Joi.Schema | Joi.ObjectSchema<T>} schema
50
+ * @param {validationOptions} [validationOptions]
51
+ * @returns {T} expected object after validation
52
+ */
53
+ validate<T>(objectToValidate: any, schema: Schema | ObjectSchema<T>, validationOptions?: ValidationOptions): T {
54
+ return this._validate<T>(objectToValidate, schema, validationOptions)
55
+ }
56
+
57
+ protected _validate<T>(objectToValidate: any, schema: Schema | ObjectSchema<T>, validationOptions?: ValidationOptions): T {
58
+ const { error: validationError, value } = schema.validate(objectToValidate, validationOptions)
59
+ if (validationError) {
60
+ throw new ErrorWithPayload(validationError.message.split('"').join("'"), validationError)
61
+ }
62
+
63
+ return value as T
64
+ }
65
+ }
@@ -0,0 +1,40 @@
1
+ import { memoizeFactory } from 'src/memoize-factory'
2
+
3
+ describe('memoizeFactory', () => {
4
+ const fake_factoryFn = jest.fn()
5
+
6
+ beforeEach(() => {
7
+ fake_factoryFn.mockImplementation((a: number, b: number): number => {
8
+ return a + b
9
+ })
10
+ })
11
+
12
+ afterEach(() => {
13
+ jest.resetAllMocks()
14
+ })
15
+
16
+ it('should only call factory function once if the same argument is passed', () => {
17
+ const memoizedImplementation = memoizeFactory(fake_factoryFn)
18
+ expect(fake_factoryFn).not.toHaveBeenCalled()
19
+
20
+ expect(memoizedImplementation(1, 2)).toEqual(3)
21
+ expect(fake_factoryFn).toHaveBeenCalledTimes(1)
22
+
23
+ expect(memoizedImplementation(1, 2)).toEqual(3)
24
+ expect(fake_factoryFn).toHaveBeenCalledTimes(1)
25
+ })
26
+
27
+ it('should call factory as many times as it is called if the arguments are different', () => {
28
+ const memoizedImplementation = memoizeFactory(fake_factoryFn)
29
+ expect(fake_factoryFn).not.toHaveBeenCalled()
30
+
31
+ expect(memoizedImplementation(1, 2)).toEqual(3)
32
+ expect(fake_factoryFn).toHaveBeenCalledTimes(1)
33
+
34
+ expect(memoizedImplementation(1, 3)).toEqual(4)
35
+ expect(fake_factoryFn).toHaveBeenCalledTimes(2)
36
+
37
+ expect(memoizedImplementation(1, 4)).toEqual(5)
38
+ expect(fake_factoryFn).toHaveBeenCalledTimes(3)
39
+ })
40
+ })
@@ -0,0 +1,27 @@
1
+ import { AnyFunction } from 'src/types/any-function'
2
+
3
+ /**
4
+ * This is a simple implementation of memoize function that caches result against the parameter passed that are passed to the
5
+ * function so it never runs the same calculation twice.
6
+ * @template F
7
+ * @template R
8
+ * @param {F} factoryFn
9
+ * @return {F: AnyFunction<R>}
10
+ * @example
11
+ * export const sumTwoNumbersMemoize = memoizeFactory((a:number, b:number): number => a + b)
12
+ *
13
+ * // using
14
+ * sumTwoNumbersMemoize(5 + 10) // 15
15
+ */
16
+ export const memoizeFactory = <F extends AnyFunction<R>, R>(factoryFn: F): F => {
17
+ const cache: { [k: string]: R } = {}
18
+
19
+ return ((...args: Parameters<F>): R => {
20
+ const key = JSON.stringify(args)
21
+ if (key in cache) {
22
+ return cache[key]
23
+ }
24
+
25
+ return (cache[key] = factoryFn(...args))
26
+ }) as F
27
+ }
@@ -0,0 +1,360 @@
1
+ import { ObjectUtil } from 'src/object-util'
2
+
3
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
4
+ describe('objectUtil', () => {
5
+ const objectUtil = new ObjectUtil()
6
+
7
+ const everyType = {
8
+ number: 1,
9
+ decimal: 1.12345,
10
+ string: 'string',
11
+ undefined: undefined,
12
+ notANumber: NaN,
13
+ emptyObj: {},
14
+ date: new Date('2020-01-01'),
15
+ boolean: true,
16
+ nestedObject: { obj: 'test' },
17
+ }
18
+
19
+ const everyTypeReversed = {
20
+ nestedObject: { obj: 'test' },
21
+ boolean: true,
22
+ date: new Date('2020-01-01'),
23
+ emptyObj: {},
24
+ notANumber: NaN,
25
+ undefined: undefined,
26
+ string: 'string',
27
+ decimal: 1.12345,
28
+ number: 1,
29
+ }
30
+
31
+ describe('deepClone', () => {
32
+ it.each([
33
+ [{ test: 'string' }],
34
+ [{ deep: { test: 'string' } }],
35
+ [{ deeper: { deep: { test: 'string' } } }],
36
+ [everyType],
37
+ [{ deep: everyType }],
38
+ [{ deeper: { deep: everyType } }],
39
+ ])('%#. should clone %j', (obj) => {
40
+ expect(objectUtil.deepClone(obj)).not.toBe(obj)
41
+ })
42
+ })
43
+
44
+ describe('pickByList', () => {
45
+ it.each([
46
+ [['a', 'b'], { a: 1, b: '2', c: 3 }, { a: 1, b: '2' }],
47
+ [['a', 'c'], { a: 1, b: '2', c: 3 }, { a: 1, c: 3 }],
48
+ ])('%#. should pick only this properties [%s] from object %s and return object %s', (propList, obj, result) => {
49
+ expect(objectUtil.pickByList(obj, propList)).toEqual(result)
50
+ })
51
+ })
52
+
53
+ describe('pickByObjectKeys', () => {
54
+ it.each([
55
+ [
56
+ { a: 1, b: '2', c: 3 },
57
+ { a: '', b: '' },
58
+ { a: 1, b: '2' },
59
+ ],
60
+ [
61
+ { a: 1, b: '2', c: 3 },
62
+ { a: '', c: '' },
63
+ { a: 1, c: 3 },
64
+ ],
65
+ ])('%#. should pick form this %s using keys from %s and return %s', (obj, objWithPickKeys, result) => {
66
+ expect(objectUtil.pickByObjectKeys(obj, objWithPickKeys)).toEqual(result)
67
+ })
68
+ })
69
+
70
+ describe('deepStringify', () => {
71
+ it.each([
72
+ [{}, '{}'],
73
+ [{ a: 1, b: 2 }, `{ a: 1, b: 2 }`],
74
+ [
75
+ everyType,
76
+ `{ boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined }`,
77
+ ],
78
+ [
79
+ { deep: everyType },
80
+ `{ deep: { boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined } }`,
81
+ ],
82
+ [
83
+ { deeper: { deep: everyType } },
84
+ `{ deeper: { deep: { boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined } } }`,
85
+ ],
86
+ ])('%#. should compare %j and sort with result %j', (value, expected) => {
87
+ expect(objectUtil.deepStringify(value, { isSorted: true })).toEqual(expected)
88
+ })
89
+
90
+ it.each([
91
+ [{ b: 2, a: 1 }, `{ a: 1, b: 2 }`],
92
+ [
93
+ everyType,
94
+ `{ boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined }`,
95
+ ],
96
+ [
97
+ { deep: everyType },
98
+ `{ deep: { boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined } }`,
99
+ ],
100
+ [
101
+ { deeper: { deep: everyType } },
102
+ `{ deeper: { deep: { boolean: true, date: 2020-01-01T00:00:00.000Z, decimal: 1.12345, emptyObj: {}, nestedObject: { obj: 'test' }, notANumber: NaN, number: 1, string: 'string', undefined: undefined } } }`,
103
+ ],
104
+ ])('%#. should compare %j with result %j and not be equal because it is not sorted', (value, expected) => {
105
+ expect(objectUtil.deepStringify(value, { isSorted: true })).toEqual(expected)
106
+ expect(objectUtil.deepStringify(value)).not.toEqual(expected)
107
+ })
108
+
109
+ it.each([
110
+ [null, 'null'],
111
+ [undefined, 'undefined'],
112
+ [123, '123'],
113
+ [[123], `[ 123 ]`],
114
+ ['test', "'test'"],
115
+ (() => {
116
+ const date = new Date()
117
+
118
+ return [date, date.toISOString()]
119
+ })(),
120
+ ])('%#. should compare %j with result %j with compact enabled', (value, expected) => {
121
+ expect(objectUtil.deepStringify(value)).toEqual(expected)
122
+ })
123
+
124
+ it.each([
125
+ [['d', 'c', 'b', 'a'], `[ 'a', 'b', 'c', 'd' ]`, `[ 'd', 'c', 'b', 'a' ]`],
126
+ [{ a: ['d', 'c', 'b', 'a'] }, `{ a: [ 'a', 'b', 'c', 'd' ]`, `{ a: [ 'd', 'c', 'b', 'a' ] }`],
127
+ ])('%# should not sort arrays', (arr, expectSorted, expectUnsorted) => {
128
+ expect(objectUtil.deepStringify(arr, { isSorted: true })).not.toEqual(expectSorted)
129
+ expect(objectUtil.deepStringify(arr, { isSorted: true })).toEqual(expectUnsorted)
130
+ })
131
+
132
+ it.each([
133
+ [
134
+ 0,
135
+ `{
136
+ a: {
137
+ a1: {
138
+ a2: {
139
+ a3: {
140
+ a4: {
141
+ a5: 'level 5'
142
+ }
143
+ }
144
+ }
145
+ }
146
+ },
147
+ b: {
148
+ b1: {
149
+ b2: {
150
+ b3: {
151
+ b4: 'level 4'
152
+ }
153
+ }
154
+ }
155
+ },
156
+ c: [
157
+ 'c0',
158
+ [
159
+ 'c1',
160
+ [
161
+ 'c2',
162
+ [
163
+ 'c3',
164
+ [
165
+ 'c4',
166
+ [
167
+ 'c5'
168
+ ]
169
+ ]
170
+ ]
171
+ ]
172
+ ]
173
+ ]
174
+ }`,
175
+ ],
176
+ [
177
+ 1,
178
+ `{
179
+ a: {
180
+ a1: {
181
+ a2: {
182
+ a3: {
183
+ a4: { a5: 'level 5' }
184
+ }
185
+ }
186
+ }
187
+ },
188
+ b: {
189
+ b1: {
190
+ b2: {
191
+ b3: { b4: 'level 4' }
192
+ }
193
+ }
194
+ },
195
+ c: [
196
+ 'c0',
197
+ [
198
+ 'c1',
199
+ [
200
+ 'c2',
201
+ [
202
+ 'c3',
203
+ [
204
+ 'c4',
205
+ [ 'c5' ]
206
+ ]
207
+ ]
208
+ ]
209
+ ]
210
+ ]
211
+ }`,
212
+ ],
213
+ [
214
+ 2,
215
+ `{
216
+ a: {
217
+ a1: {
218
+ a2: {
219
+ a3: { a4: { a5: 'level 5' } }
220
+ }
221
+ }
222
+ },
223
+ b: {
224
+ b1: {
225
+ b2: { b3: { b4: 'level 4' } }
226
+ }
227
+ },
228
+ c: [
229
+ 'c0',
230
+ [
231
+ 'c1',
232
+ [
233
+ 'c2',
234
+ [
235
+ 'c3',
236
+ [ 'c4', [ 'c5' ] ]
237
+ ]
238
+ ]
239
+ ]
240
+ ]
241
+ }`,
242
+ ],
243
+ [
244
+ 3,
245
+ `{
246
+ a: {
247
+ a1: {
248
+ a2: { a3: { a4: { a5: 'level 5' } } }
249
+ }
250
+ },
251
+ b: {
252
+ b1: { b2: { b3: { b4: 'level 4' } } }
253
+ },
254
+ c: [
255
+ 'c0',
256
+ [
257
+ 'c1',
258
+ [
259
+ 'c2',
260
+ [ 'c3', [ 'c4', [ 'c5' ] ] ]
261
+ ]
262
+ ]
263
+ ]
264
+ }`,
265
+ ],
266
+ [
267
+ 4,
268
+ `{
269
+ a: {
270
+ a1: { a2: { a3: { a4: { a5: 'level 5' } } } }
271
+ },
272
+ b: { b1: { b2: { b3: { b4: 'level 4' } } } },
273
+ c: [
274
+ 'c0',
275
+ [
276
+ 'c1',
277
+ [ 'c2', [ 'c3', [ 'c4', [ 'c5' ] ] ] ]
278
+ ]
279
+ ]
280
+ }`,
281
+ ],
282
+ [
283
+ 5,
284
+ `{
285
+ a: { a1: { a2: { a3: { a4: { a5: 'level 5' } } } } },
286
+ b: { b1: { b2: { b3: { b4: 'level 4' } } } },
287
+ c: [
288
+ 'c0',
289
+ [ 'c1', [ 'c2', [ 'c3', [ 'c4', [ 'c5' ] ] ] ] ]
290
+ ]
291
+ }`,
292
+ ],
293
+ [
294
+ 6,
295
+ `{
296
+ a: { a1: { a2: { a3: { a4: { a5: 'level 5' } } } } },
297
+ b: { b1: { b2: { b3: { b4: 'level 4' } } } },
298
+ c: [ 'c0', [ 'c1', [ 'c2', [ 'c3', [ 'c4', [ 'c5' ] ] ] ] ] ]
299
+ }`,
300
+ ],
301
+ ])('%# should take object with 5 levels and compact it to level $s', (level, expected) => {
302
+ const obj = {
303
+ a: { a1: { a2: { a3: { a4: { a5: 'level 5' } } } } },
304
+ b: { b1: { b2: { b3: { b4: 'level 4' } } } },
305
+ c: ['c0', ['c1', ['c2', ['c3', ['c4', ['c5']]]]]],
306
+ }
307
+ expect(objectUtil.deepStringify(obj, { isPrettyPrinted: true, prettyPrintCompactLevel: level })).toEqual(expected)
308
+ })
309
+ })
310
+
311
+ describe('deepEqual', () => {
312
+ it.each([
313
+ [null, null],
314
+ [undefined, undefined],
315
+ [{}, {}],
316
+ [everyType, everyType],
317
+ [{ deep: everyType }, { deep: everyType }],
318
+ [{ deeper: { deep: everyType } }, { deeper: { deep: everyType } }],
319
+ ])('%#. should be deep equal compare %j with result %j', (value, result) => {
320
+ expect(objectUtil.deepEqual(value, result)).toBeTruthy()
321
+ })
322
+
323
+ it.each([
324
+ [{}, { a: 1 }],
325
+ [everyType, { ...everyType, a: 1 }],
326
+ [{ deep: everyType }, { a: 1, deep: everyType }],
327
+ [{ deeper: { deep: everyType } }, { a: 1, deeper: { deep: everyType } }],
328
+ ])('%#. should not deep equal compare %j with result %j', (value, result) => {
329
+ expect(objectUtil.deepEqual(value, result)).toBeFalsy()
330
+ })
331
+
332
+ it.each([
333
+ [
334
+ { a: 1, b: 2 },
335
+ { a: 1, b: 2 },
336
+ ],
337
+ [
338
+ { a: { a: 1, b: 2 }, b: everyType },
339
+ { a: { a: 1, b: 2 }, b: everyTypeReversed },
340
+ ],
341
+ ])('%#. should be deep equal even if in different order compare %j with result %j', (value, result) => {
342
+ expect(objectUtil.deepEqual(value, result)).toBeTruthy()
343
+ })
344
+ })
345
+
346
+ describe('deepNullToUndefined', () => {
347
+ it.each([
348
+ [{ test: undefined }, { test: undefined }],
349
+ [{ test: null }, { test: undefined }],
350
+ [{ deep: { test: null } }, { deep: { test: undefined } }],
351
+ [{ deeper: { deep: { test: null } } }, { deeper: { deep: { test: undefined } } }],
352
+ [everyType, everyType],
353
+ [{ deep: everyType }, { deep: everyType }],
354
+ [{ deeper: { deep: everyType } }, { deeper: { deep: everyType } }],
355
+ ])('%#. should convert null to undefined for %j', (withNulls, withUndefined) => {
356
+ expect(objectUtil.deepNullToUndefined(withNulls)).toEqual(withUndefined)
357
+ })
358
+ })
359
+ })
360
+ /* eslint-enable sort-keys-fix/sort-keys-fix */