• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 15219332477

23 May 2025 09:13PM UTC coverage: 17.216% (-59.1%) from 76.346%
15219332477

Pull #11332

github

naorpeled
cr comments - move if block
Pull Request #11332: feat: add new undefined and null behavior flags

1603 of 12759 branches covered (12.56%)

Branch coverage included in aggregate %.

0 of 31 new or added lines in 3 files covered. (0.0%)

14132 existing lines in 166 files now uncovered.

4731 of 24033 relevant lines covered (19.69%)

60.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.39
/src/util/OrmUtils.ts
1
import { ObjectLiteral } from "../common/ObjectLiteral"
2
import {
3
    PrimitiveCriteria,
4
    SinglePrimitiveCriteria,
5
} from "../common/PrimitiveCriteria"
6

7
export class OrmUtils {
1✔
8
    // -------------------------------------------------------------------------
9
    // Public methods
10
    // -------------------------------------------------------------------------
11

12
    /**
13
     * Chunks array into pieces.
14
     */
15
    public static chunk<T>(array: T[], size: number): T[][] {
UNCOV
16
        return Array.from(Array(Math.ceil(array.length / size)), (_, i) => {
×
UNCOV
17
            return array.slice(i * size, i * size + size)
×
18
        })
19
    }
20

21
    public static splitClassesAndStrings<T>(
22
        classesAndStrings: (string | T)[],
23
    ): [T[], string[]] {
24
        return [
100✔
25
            classesAndStrings.filter(
26
                (cls): cls is T => typeof cls !== "string",
54✔
27
            ),
28
            classesAndStrings.filter(
29
                (str): str is string => typeof str === "string",
54✔
30
            ),
31
        ]
32
    }
33

34
    public static groupBy<T, R>(
35
        array: T[],
36
        propertyCallback: (item: T) => R,
37
    ): { id: R; items: T[] }[] {
38
        return array.reduce((groupedArray, value) => {
×
39
            const key = propertyCallback(value)
×
40
            let grouped = groupedArray.find((i) => i.id === key)
×
41
            if (!grouped) {
×
42
                grouped = { id: key, items: [] }
×
43
                groupedArray.push(grouped)
×
44
            }
45
            grouped.items.push(value)
×
46
            return groupedArray
×
47
        }, [] as Array<{ id: R; items: T[] }>)
48
    }
49

50
    public static uniq<T>(array: T[], criteria?: (item: T) => any): T[]
51
    public static uniq<T, K extends keyof T>(array: T[], property: K): T[]
52
    public static uniq<T, K extends keyof T>(
53
        array: T[],
54
        criteriaOrProperty?: ((item: T) => any) | K,
55
    ): T[] {
UNCOV
56
        return array.reduce((uniqueArray, item) => {
×
UNCOV
57
            let found: boolean = false
×
UNCOV
58
            if (typeof criteriaOrProperty === "function") {
×
UNCOV
59
                const itemValue = criteriaOrProperty(item)
×
UNCOV
60
                found = !!uniqueArray.find(
×
61
                    (uniqueItem) =>
UNCOV
62
                        criteriaOrProperty(uniqueItem) === itemValue,
×
63
                )
64
            } else if (typeof criteriaOrProperty === "string") {
×
65
                found = !!uniqueArray.find(
×
66
                    (uniqueItem) =>
67
                        uniqueItem[criteriaOrProperty] ===
×
68
                        item[criteriaOrProperty],
69
                )
70
            } else {
71
                found = uniqueArray.indexOf(item) !== -1
×
72
            }
73

UNCOV
74
            if (!found) uniqueArray.push(item)
×
75

UNCOV
76
            return uniqueArray
×
77
        }, [] as T[])
78
    }
79

80
    /**
81
     * Deep Object.assign.
82
     */
83
    public static mergeDeep(target: any, ...sources: any[]): any {
84
        if (!sources.length) {
1,047!
85
            return target
×
86
        }
87

88
        for (const source of sources) {
1,047✔
89
            OrmUtils.merge(target, source)
1,065✔
90
        }
91

92
        return target
1,047✔
93
    }
94

95
    /**
96
     * Deep compare objects.
97
     *
98
     * @see http://stackoverflow.com/a/1144249
99
     */
100
    public static deepCompare(...args: any[]): boolean {
101
        let i: any, l: any, leftChain: any, rightChain: any
102

103
        if (arguments.length < 1) {
20!
104
            return true // Die silently? Don't know how to handle such case, please help...
×
105
            // throw "Need two or more arguments to compare";
106
        }
107

108
        for (i = 1, l = arguments.length; i < l; i++) {
20✔
109
            leftChain = [] // Todo: this can be cached
20✔
110
            rightChain = []
20✔
111

112
            if (
20!
113
                !this.compare2Objects(
114
                    leftChain,
115
                    rightChain,
116
                    arguments[0],
117
                    arguments[i],
118
                )
119
            ) {
UNCOV
120
                return false
×
121
            }
122
        }
123

124
        return true
20✔
125
    }
126

127
    /**
128
     * Gets deeper value of object.
129
     */
130
    public static deepValue(obj: ObjectLiteral, path: string) {
UNCOV
131
        const segments = path.split(".")
×
UNCOV
132
        for (let i = 0, len = segments.length; i < len; i++) {
×
UNCOV
133
            obj = obj[segments[i]]
×
134
        }
UNCOV
135
        return obj
×
136
    }
137

138
    public static replaceEmptyObjectsWithBooleans(obj: any) {
UNCOV
139
        for (const key in obj) {
×
UNCOV
140
            if (obj[key] && typeof obj[key] === "object") {
×
UNCOV
141
                if (Object.keys(obj[key]).length === 0) {
×
UNCOV
142
                    obj[key] = true
×
143
                } else {
UNCOV
144
                    this.replaceEmptyObjectsWithBooleans(obj[key])
×
145
                }
146
            }
147
        }
148
    }
149

150
    public static propertyPathsToTruthyObject(paths: string[]) {
UNCOV
151
        const obj: any = {}
×
UNCOV
152
        for (const path of paths) {
×
UNCOV
153
            const props = path.split(".")
×
UNCOV
154
            if (!props.length) continue
×
155

UNCOV
156
            if (!obj[props[0]] || obj[props[0]] === true) {
×
UNCOV
157
                obj[props[0]] = {}
×
158
            }
UNCOV
159
            let recursiveChild = obj[props[0]]
×
UNCOV
160
            for (const [key, prop] of props.entries()) {
×
UNCOV
161
                if (key === 0) continue
×
162

UNCOV
163
                if (recursiveChild[prop]) {
×
164
                    recursiveChild = recursiveChild[prop]
×
UNCOV
165
                } else if (key === props.length - 1) {
×
UNCOV
166
                    recursiveChild[prop] = {}
×
UNCOV
167
                    recursiveChild = null
×
168
                } else {
169
                    recursiveChild[prop] = {}
×
170
                    recursiveChild = recursiveChild[prop]
×
171
                }
172
            }
173
        }
UNCOV
174
        this.replaceEmptyObjectsWithBooleans(obj)
×
UNCOV
175
        return obj
×
176
    }
177

178
    /**
179
     * Check if two entity-id-maps are the same
180
     */
181
    public static compareIds(
182
        firstId: ObjectLiteral | undefined,
183
        secondId: ObjectLiteral | undefined,
184
    ): boolean {
185
        if (
20!
186
            firstId === undefined ||
80✔
187
            firstId === null ||
188
            secondId === undefined ||
189
            secondId === null
190
        )
UNCOV
191
            return false
×
192

193
        // Optimized version for the common case
194
        if (
20!
195
            ((typeof firstId.id === "string" &&
40!
196
                typeof secondId.id === "string") ||
197
                (typeof firstId.id === "number" &&
198
                    typeof secondId.id === "number")) &&
199
            Object.keys(firstId).length === 1 &&
200
            Object.keys(secondId).length === 1
201
        ) {
UNCOV
202
            return firstId.id === secondId.id
×
203
        }
204

205
        return OrmUtils.deepCompare(firstId, secondId)
20✔
206
    }
207

208
    /**
209
     * Transforms given value into boolean value.
210
     */
211
    public static toBoolean(value: any): boolean {
212
        if (typeof value === "boolean") return value
9!
213

214
        if (typeof value === "string") return value === "true" || value === "1"
9!
215

216
        if (typeof value === "number") return value > 0
9!
217

218
        return false
9✔
219
    }
220

221
    /**
222
     * Composes an object from the given array of keys and values.
223
     */
224
    public static zipObject(keys: any[], values: any[]): ObjectLiteral {
225
        return keys.reduce((object, column, index) => {
×
226
            object[column] = values[index]
×
227
            return object
×
228
        }, {} as ObjectLiteral)
229
    }
230

231
    /**
232
     * Compares two arrays.
233
     */
234
    public static isArraysEqual(arr1: any[], arr2: any[]): boolean {
UNCOV
235
        if (arr1.length !== arr2.length) return false
×
UNCOV
236
        return arr1.every((element) => {
×
UNCOV
237
            return arr2.indexOf(element) !== -1
×
238
        })
239
    }
240

241
    public static areMutuallyExclusive<T>(...lists: T[][]): boolean {
242
        const haveSharedObjects = lists.some((list) => {
×
243
            const otherLists = lists.filter((otherList) => otherList !== list)
×
244
            return list.some((item) =>
×
245
                otherLists.some((otherList) => otherList.includes(item)),
×
246
            )
247
        })
248
        return !haveSharedObjects
×
249
    }
250

251
    /**
252
     * Parses the CHECK constraint on the specified column and returns
253
     * all values allowed by the constraint or undefined if the constraint
254
     * is not present.
255
     */
256
    public static parseSqlCheckExpression(
257
        sql: string,
258
        columnName: string,
259
    ): string[] | undefined {
260
        const enumMatch = sql.match(
4✔
261
            new RegExp(
262
                `"${columnName}" varchar CHECK\\s*\\(\\s*"${columnName}"\\s+IN\\s*`,
263
            ),
264
        )
265

266
        if (enumMatch && enumMatch.index) {
4✔
267
            const afterMatch = sql.substring(
3✔
268
                enumMatch.index + enumMatch[0].length,
269
            )
270

271
            // This is an enum
272
            // all enum values stored as a comma separated list
273
            const chars = afterMatch
3✔
274

275
            /**
276
             * * When outside quotes: empty string
277
             * * When inside single quotes: `'`
278
             */
279
            let currentQuotes = ""
3✔
280
            let nextValue = ""
3✔
281
            const enumValues: string[] = []
3✔
282
            for (let idx = 0; idx < chars.length; idx++) {
3✔
283
                const char = chars[idx]
375✔
284
                switch (char) {
375✔
285
                    case ",":
286
                        if (currentQuotes == "") {
14✔
287
                            enumValues.push(nextValue)
10✔
288
                            nextValue = ""
10✔
289
                        } else {
290
                            nextValue += char
4✔
291
                        }
292
                        break
14✔
293
                    case "'":
294
                        if (currentQuotes == char) {
33✔
295
                            const isNextCharQuote = chars[idx + 1] === char
20✔
296
                            if (isNextCharQuote) {
20✔
297
                                // double quote in sql should be treated as a
298
                                // single quote that's part of the quoted string
299
                                nextValue += char
7✔
300
                                idx += 1 // skip that next quote
7✔
301
                            } else {
302
                                currentQuotes = ""
13✔
303
                            }
304
                        } else {
305
                            currentQuotes = char
13✔
306
                        }
307
                        break
33✔
308
                    case ")":
309
                        if (currentQuotes == "") {
5✔
310
                            enumValues.push(nextValue)
3✔
311
                            return enumValues
3✔
312
                        } else {
313
                            nextValue += char
2✔
314
                        }
315
                        break
2✔
316
                    default:
317
                        if (currentQuotes != "") {
323✔
318
                            nextValue += char
26✔
319
                        }
320
                }
321
            }
322
        }
323
        return undefined
1✔
324
    }
325

326
    /**
327
     * Checks if given criteria is null or empty.
328
     */
329
    public static isCriteriaNullOrEmpty(criteria: unknown): boolean {
UNCOV
330
        return (
×
331
            criteria === undefined ||
×
332
            criteria === null ||
333
            criteria === "" ||
334
            (Array.isArray(criteria) && criteria.length === 0) ||
335
            (this.isPlainObject(criteria) && Object.keys(criteria).length === 0)
336
        )
337
    }
338

339
    /**
340
     * Checks if given criteria is a primitive value.
341
     * Primitive values are strings, numbers and dates.
342
     */
343
    public static isSinglePrimitiveCriteria(
344
        criteria: unknown,
345
    ): criteria is SinglePrimitiveCriteria {
UNCOV
346
        return (
×
347
            typeof criteria === "string" ||
×
348
            typeof criteria === "number" ||
349
            criteria instanceof Date
350
        )
351
    }
352

353
    /**
354
     * Checks if given criteria is a primitive value or an array of primitive values.
355
     */
356
    public static isPrimitiveCriteria(
357
        criteria: unknown,
358
    ): criteria is PrimitiveCriteria {
UNCOV
359
        if (Array.isArray(criteria)) {
×
UNCOV
360
            return criteria.every((value) =>
×
UNCOV
361
                this.isSinglePrimitiveCriteria(value),
×
362
            )
363
        }
364

UNCOV
365
        return this.isSinglePrimitiveCriteria(criteria)
×
366
    }
367

368
    // -------------------------------------------------------------------------
369
    // Private methods
370
    // -------------------------------------------------------------------------
371

372
    private static compare2Objects(
373
        leftChain: any,
374
        rightChain: any,
375
        x: any,
376
        y: any,
377
    ) {
378
        let p
379

380
        // remember that NaN === NaN returns false
381
        // and isNaN(undefined) returns true
382
        if (Number.isNaN(x) && Number.isNaN(y)) return true
40!
383

384
        // Compare primitives and functions.
385
        // Check if both arguments link to the same object.
386
        // Especially useful on the step where we compare prototypes
387
        if (x === y) return true
40!
388

389
        // Unequal, but either is null or undefined (use case: jsonb comparison)
390
        // PR #3776, todo: add tests
391
        if (x === null || y === null || x === undefined || y === undefined)
40!
UNCOV
392
            return false
×
393

394
        // Fix the buffer compare bug.
395
        // See: https://github.com/typeorm/typeorm/issues/3654
396
        if (
40✔
397
            (typeof x.equals === "function" ||
80✔
398
                typeof x.equals === "function") &&
399
            x.equals(y)
400
        )
401
            return true
20✔
402

403
        // Works in case when functions are created in constructor.
404
        // Comparing dates is a common scenario. Another built-ins?
405
        // We can even handle functions passed across iframes
406
        if (
20!
407
            (typeof x === "function" && typeof y === "function") ||
100!
408
            (x instanceof Date && y instanceof Date) ||
409
            (x instanceof RegExp && y instanceof RegExp) ||
410
            (typeof x === "string" && typeof y === "string") ||
411
            (typeof x === "number" && typeof y === "number")
412
        )
UNCOV
413
            return x.toString() === y.toString()
×
414

415
        // At last checking prototypes as good as we can
416
        if (!(typeof x === "object" && typeof y === "object")) return false
20!
417

418
        if (
20!
419
            Object.prototype.isPrototypeOf.call(x, y) ||
40✔
420
            Object.prototype.isPrototypeOf.call(y, x)
421
        )
422
            return false
×
423

424
        if (x.constructor !== y.constructor) return false
20!
425

426
        if (x.prototype !== y.prototype) return false
20!
427

428
        // Check for infinitive linking loops
429
        if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1)
20!
430
            return false
×
431

432
        // Quick checking of one object being a subset of another.
433
        // todo: cache the structure of arguments[0] for performance
434
        for (p in y) {
20✔
435
            if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
20!
UNCOV
436
                return false
×
437
            } else if (typeof y[p] !== typeof x[p]) {
20!
UNCOV
438
                return false
×
439
            }
440
        }
441

442
        for (p in x) {
20✔
443
            if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
20!
444
                return false
×
445
            } else if (typeof y[p] !== typeof x[p]) {
20!
446
                return false
×
447
            }
448

449
            switch (typeof x[p]) {
20!
450
                case "object":
451
                case "function":
452
                    leftChain.push(x)
20✔
453
                    rightChain.push(y)
20✔
454

455
                    if (
20!
456
                        !this.compare2Objects(leftChain, rightChain, x[p], y[p])
457
                    ) {
UNCOV
458
                        return false
×
459
                    }
460

461
                    leftChain.pop()
20✔
462
                    rightChain.pop()
20✔
463
                    break
20✔
464

465
                default:
UNCOV
466
                    if (x[p] !== y[p]) {
×
UNCOV
467
                        return false
×
468
                    }
UNCOV
469
                    break
×
470
            }
471
        }
472

473
        return true
20✔
474
    }
475

476
    // Checks if it's an object made by Object.create(null), {} or new Object()
477
    private static isPlainObject(item: any) {
478
        if (item === null || item === undefined) {
3,743✔
479
            return false
28✔
480
        }
481

482
        return !item.constructor || item.constructor === Object
3,715✔
483
    }
484

485
    private static mergeArrayKey(
486
        target: any,
487
        key: number,
488
        value: any,
489
        memo: Map<any, any>,
490
    ) {
491
        // Have we seen this before?  Prevent infinite recursion.
492
        if (memo.has(value)) {
49!
493
            target[key] = memo.get(value)
×
494
            return
×
495
        }
496

497
        if (value instanceof Promise) {
49!
498
            // Skip promises entirely.
499
            // This is a hold-over from the old code & is because we don't want to pull in
500
            // the lazy fields.  Ideally we'd remove these promises via another function first
501
            // but for now we have to do it here.
502
            return
×
503
        }
504

505
        if (!this.isPlainObject(value) && !Array.isArray(value)) {
49✔
506
            target[key] = value
43✔
507
            return
43✔
508
        }
509

510
        if (!target[key]) {
6✔
511
            target[key] = Array.isArray(value) ? [] : {}
3!
512
        }
513

514
        memo.set(value, target[key])
6✔
515
        this.merge(target[key], value, memo)
6✔
516
        memo.delete(value)
6✔
517
    }
518

519
    private static mergeObjectKey(
520
        target: any,
521
        key: string,
522
        value: any,
523
        memo: Map<any, any>,
524
    ) {
525
        // Have we seen this before?  Prevent infinite recursion.
526
        if (memo.has(value)) {
1,240✔
527
            Object.assign(target, { [key]: memo.get(value) })
4✔
528
            return
4✔
529
        }
530

531
        if (value instanceof Promise) {
1,236✔
532
            // Skip promises entirely.
533
            // This is a hold-over from the old code & is because we don't want to pull in
534
            // the lazy fields.  Ideally we'd remove these promises via another function first
535
            // but for now we have to do it here.
536
            return
4✔
537
        }
538

539
        if (!this.isPlainObject(value) && !Array.isArray(value)) {
1,232✔
540
            Object.assign(target, { [key]: value })
1,056✔
541
            return
1,056✔
542
        }
543

544
        if (!target[key]) {
176✔
545
            Object.assign(target, { [key]: Array.isArray(value) ? [] : {} })
66✔
546
        }
547

548
        memo.set(value, target[key])
176✔
549
        this.merge(target[key], value, memo)
176✔
550
        memo.delete(value)
176✔
551
    }
552

553
    private static merge(
554
        target: any,
555
        source: any,
556
        memo: Map<any, any> = new Map(),
1,065✔
557
    ): any {
558
        if (this.isPlainObject(target) && this.isPlainObject(source)) {
1,247✔
559
            for (const key of Object.keys(source)) {
1,202✔
560
                if (key === "__proto__") continue
1,240!
561
                this.mergeObjectKey(target, key, source[key], memo)
1,240✔
562
            }
563
        }
564

565
        if (Array.isArray(target) && Array.isArray(source)) {
1,247✔
566
            for (let key = 0; key < source.length; key++) {
22✔
567
                this.mergeArrayKey(target, key, source[key], memo)
49✔
568
            }
569
        }
570
    }
571
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc