• 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

5.37
/src/query-builder/SoftDeleteQueryBuilder.ts
1
import { QueryBuilder } from "./QueryBuilder"
1✔
2
import { ObjectLiteral } from "../common/ObjectLiteral"
3
import { EntityTarget } from "../common/EntityTarget"
4
import { DataSource } from "../data-source/DataSource"
5
import { QueryRunner } from "../query-runner/QueryRunner"
6
import { WhereExpressionBuilder } from "./WhereExpressionBuilder"
7
import { Brackets } from "./Brackets"
8
import { UpdateResult } from "./result/UpdateResult"
1✔
9
import { ReturningStatementNotSupportedError } from "../error/ReturningStatementNotSupportedError"
1✔
10
import { ReturningResultsEntityUpdator } from "./ReturningResultsEntityUpdator"
1✔
11
import { OrderByCondition } from "../find-options/OrderByCondition"
12
import { LimitOnUpdateNotSupportedError } from "../error/LimitOnUpdateNotSupportedError"
1✔
13
import { MissingDeleteDateColumnError } from "../error/MissingDeleteDateColumnError"
1✔
14
import { UpdateValuesMissingError } from "../error/UpdateValuesMissingError"
1✔
15
import { TypeORMError } from "../error"
1✔
16
import { DriverUtils } from "../driver/DriverUtils"
1✔
17
import { InstanceChecker } from "../util/InstanceChecker"
1✔
18

19
/**
20
 * Allows to build complex sql queries in a fashion way and execute those queries.
21
 */
22
export class SoftDeleteQueryBuilder<Entity extends ObjectLiteral>
1✔
23
    extends QueryBuilder<Entity>
24
    implements WhereExpressionBuilder
25
{
UNCOV
26
    readonly "@instanceof" = Symbol.for("SoftDeleteQueryBuilder")
×
27

28
    // -------------------------------------------------------------------------
29
    // Constructor
30
    // -------------------------------------------------------------------------
31

32
    constructor(
33
        connectionOrQueryBuilder: DataSource | QueryBuilder<any>,
34
        queryRunner?: QueryRunner,
35
    ) {
UNCOV
36
        super(connectionOrQueryBuilder as any, queryRunner)
×
UNCOV
37
        this.expressionMap.aliasNamePrefixingEnabled = false
×
38
    }
39

40
    // -------------------------------------------------------------------------
41
    // Public Implemented Methods
42
    // -------------------------------------------------------------------------
43

44
    /**
45
     * Gets generated SQL query without parameters being replaced.
46
     */
47
    getQuery(): string {
UNCOV
48
        let sql = this.createUpdateExpression()
×
UNCOV
49
        sql += this.createCteExpression()
×
UNCOV
50
        sql += this.createOrderByExpression()
×
UNCOV
51
        sql += this.createLimitExpression()
×
UNCOV
52
        return this.replacePropertyNamesForTheWholeQuery(sql.trim())
×
53
    }
54

55
    /**
56
     * Executes sql generated by query builder and returns raw database results.
57
     */
58
    async execute(): Promise<UpdateResult> {
UNCOV
59
        const queryRunner = this.obtainQueryRunner()
×
UNCOV
60
        let transactionStartedByUs: boolean = false
×
61

UNCOV
62
        try {
×
63
            // start transaction if it was enabled
UNCOV
64
            if (
×
65
                this.expressionMap.useTransaction === true &&
×
66
                queryRunner.isTransactionActive === false
67
            ) {
68
                await queryRunner.startTransaction()
×
69
                transactionStartedByUs = true
×
70
            }
71

72
            // call before soft remove and recover methods in listeners and subscribers
UNCOV
73
            if (
×
74
                this.expressionMap.callListeners === true &&
×
75
                this.expressionMap.mainAlias!.hasMetadata
76
            ) {
UNCOV
77
                if (this.expressionMap.queryType === "soft-delete")
×
UNCOV
78
                    await queryRunner.broadcaster.broadcast(
×
79
                        "BeforeSoftRemove",
80
                        this.expressionMap.mainAlias!.metadata,
81
                    )
UNCOV
82
                else if (this.expressionMap.queryType === "restore")
×
UNCOV
83
                    await queryRunner.broadcaster.broadcast(
×
84
                        "BeforeRecover",
85
                        this.expressionMap.mainAlias!.metadata,
86
                    )
87
            }
88

89
            // if update entity mode is enabled we may need extra columns for the returning statement
90
            const returningResultsEntityUpdator =
UNCOV
91
                new ReturningResultsEntityUpdator(
×
92
                    queryRunner,
93
                    this.expressionMap,
94
                )
UNCOV
95
            if (
×
96
                this.expressionMap.updateEntity === true &&
×
97
                this.expressionMap.mainAlias!.hasMetadata &&
98
                this.expressionMap.whereEntities.length > 0
99
            ) {
UNCOV
100
                this.expressionMap.extraReturningColumns =
×
101
                    returningResultsEntityUpdator.getSoftDeletionReturningColumns()
102
            }
103

104
            // execute update query
UNCOV
105
            const [sql, parameters] = this.getQueryAndParameters()
×
106

UNCOV
107
            const queryResult = await queryRunner.query(sql, parameters, true)
×
UNCOV
108
            const updateResult = UpdateResult.from(queryResult)
×
109

110
            // if we are updating entities and entity updation is enabled we must update some of entity columns (like version, update date, etc.)
UNCOV
111
            if (
×
112
                this.expressionMap.updateEntity === true &&
×
113
                this.expressionMap.mainAlias!.hasMetadata &&
114
                this.expressionMap.whereEntities.length > 0
115
            ) {
UNCOV
116
                await returningResultsEntityUpdator.update(
×
117
                    updateResult,
118
                    this.expressionMap.whereEntities,
119
                )
120
            }
121

122
            // call after soft remove and recover methods in listeners and subscribers
UNCOV
123
            if (
×
124
                this.expressionMap.callListeners === true &&
×
125
                this.expressionMap.mainAlias!.hasMetadata
126
            ) {
UNCOV
127
                if (this.expressionMap.queryType === "soft-delete")
×
UNCOV
128
                    await queryRunner.broadcaster.broadcast(
×
129
                        "AfterSoftRemove",
130
                        this.expressionMap.mainAlias!.metadata,
131
                    )
UNCOV
132
                else if (this.expressionMap.queryType === "restore")
×
UNCOV
133
                    await queryRunner.broadcaster.broadcast(
×
134
                        "AfterRecover",
135
                        this.expressionMap.mainAlias!.metadata,
136
                    )
137
            }
138

139
            // close transaction if we started it
UNCOV
140
            if (transactionStartedByUs) await queryRunner.commitTransaction()
×
141

UNCOV
142
            return updateResult
×
143
        } catch (error) {
144
            // rollback transaction if we started it
UNCOV
145
            if (transactionStartedByUs) {
×
146
                try {
×
147
                    await queryRunner.rollbackTransaction()
×
148
                } catch (rollbackError) {}
149
            }
UNCOV
150
            throw error
×
151
        } finally {
UNCOV
152
            if (queryRunner !== this.queryRunner) {
×
153
                // means we created our own query runner
UNCOV
154
                await queryRunner.release()
×
155
            }
156
        }
157
    }
158

159
    // -------------------------------------------------------------------------
160
    // Public Methods
161
    // -------------------------------------------------------------------------
162

163
    /**
164
     * Specifies FROM which entity's table select/update/delete/soft-delete will be executed.
165
     * Also sets a main string alias of the selection data.
166
     */
167
    from<T extends ObjectLiteral>(
168
        entityTarget: EntityTarget<T>,
169
        aliasName?: string,
170
    ): SoftDeleteQueryBuilder<T> {
UNCOV
171
        entityTarget = InstanceChecker.isEntitySchema(entityTarget)
×
172
            ? entityTarget.options.name
173
            : entityTarget
UNCOV
174
        const mainAlias = this.createFromAlias(entityTarget, aliasName)
×
UNCOV
175
        this.expressionMap.setMainAlias(mainAlias)
×
UNCOV
176
        return this as any as SoftDeleteQueryBuilder<T>
×
177
    }
178

179
    /**
180
     * Sets WHERE condition in the query builder.
181
     * If you had previously WHERE expression defined,
182
     * calling this function will override previously set WHERE conditions.
183
     * Additionally you can add parameters used in where expression.
184
     */
185
    where(
186
        where:
187
            | string
188
            | ((qb: this) => string)
189
            | Brackets
190
            | ObjectLiteral
191
            | ObjectLiteral[],
192
        parameters?: ObjectLiteral,
193
    ): this {
UNCOV
194
        this.expressionMap.wheres = [] // don't move this block below since computeWhereParameter can add where expressions
×
UNCOV
195
        const condition = this.getWhereCondition(where)
×
UNCOV
196
        if (condition)
×
UNCOV
197
            this.expressionMap.wheres = [
×
198
                { type: "simple", condition: condition },
199
            ]
UNCOV
200
        if (parameters) this.setParameters(parameters)
×
UNCOV
201
        return this
×
202
    }
203

204
    /**
205
     * Adds new AND WHERE condition in the query builder.
206
     * Additionally you can add parameters used in where expression.
207
     */
208
    andWhere(
209
        where:
210
            | string
211
            | ((qb: this) => string)
212
            | Brackets
213
            | ObjectLiteral
214
            | ObjectLiteral[],
215
        parameters?: ObjectLiteral,
216
    ): this {
217
        this.expressionMap.wheres.push({
×
218
            type: "and",
219
            condition: this.getWhereCondition(where),
220
        })
221
        if (parameters) this.setParameters(parameters)
×
222
        return this
×
223
    }
224

225
    /**
226
     * Adds new OR WHERE condition in the query builder.
227
     * Additionally you can add parameters used in where expression.
228
     */
229
    orWhere(
230
        where:
231
            | string
232
            | ((qb: this) => string)
233
            | Brackets
234
            | ObjectLiteral
235
            | ObjectLiteral[],
236
        parameters?: ObjectLiteral,
237
    ): this {
UNCOV
238
        this.expressionMap.wheres.push({
×
239
            type: "or",
240
            condition: this.getWhereCondition(where),
241
        })
UNCOV
242
        if (parameters) this.setParameters(parameters)
×
UNCOV
243
        return this
×
244
    }
245

246
    /**
247
     * Adds new AND WHERE with conditions for the given ids.
248
     */
249
    whereInIds(ids: any | any[]): this {
250
        return this.where(this.getWhereInIdsCondition(ids))
×
251
    }
252

253
    /**
254
     * Adds new AND WHERE with conditions for the given ids.
255
     */
256
    andWhereInIds(ids: any | any[]): this {
257
        return this.andWhere(this.getWhereInIdsCondition(ids))
×
258
    }
259

260
    /**
261
     * Adds new OR WHERE with conditions for the given ids.
262
     */
263
    orWhereInIds(ids: any | any[]): this {
UNCOV
264
        return this.orWhere(this.getWhereInIdsCondition(ids))
×
265
    }
266
    /**
267
     * Optional returning/output clause.
268
     * This will return given column values.
269
     */
270
    output(columns: string[]): this
271

272
    /**
273
     * Optional returning/output clause.
274
     * Returning is a SQL string containing returning statement.
275
     */
276
    output(output: string): this
277

278
    /**
279
     * Optional returning/output clause.
280
     */
281
    output(output: string | string[]): this
282

283
    /**
284
     * Optional returning/output clause.
285
     */
286
    output(output: string | string[]): this {
287
        return this.returning(output)
×
288
    }
289

290
    /**
291
     * Optional returning/output clause.
292
     * This will return given column values.
293
     */
294
    returning(columns: string[]): this
295

296
    /**
297
     * Optional returning/output clause.
298
     * Returning is a SQL string containing returning statement.
299
     */
300
    returning(returning: string): this
301

302
    /**
303
     * Optional returning/output clause.
304
     */
305
    returning(returning: string | string[]): this
306

307
    /**
308
     * Optional returning/output clause.
309
     */
310
    returning(returning: string | string[]): this {
311
        // not all databases support returning/output cause
312
        if (!this.connection.driver.isReturningSqlSupported("update")) {
×
313
            throw new ReturningStatementNotSupportedError()
×
314
        }
315

316
        this.expressionMap.returning = returning
×
317
        return this
×
318
    }
319

320
    /**
321
     * Sets ORDER BY condition in the query builder.
322
     * If you had previously ORDER BY expression defined,
323
     * calling this function will override previously set ORDER BY conditions.
324
     *
325
     * Calling order by without order set will remove all previously set order bys.
326
     */
327
    orderBy(): this
328

329
    /**
330
     * Sets ORDER BY condition in the query builder.
331
     * If you had previously ORDER BY expression defined,
332
     * calling this function will override previously set ORDER BY conditions.
333
     */
334
    orderBy(
335
        sort: string,
336
        order?: "ASC" | "DESC",
337
        nulls?: "NULLS FIRST" | "NULLS LAST",
338
    ): this
339

340
    /**
341
     * Sets ORDER BY condition in the query builder.
342
     * If you had previously ORDER BY expression defined,
343
     * calling this function will override previously set ORDER BY conditions.
344
     */
345
    orderBy(order: OrderByCondition): this
346

347
    /**
348
     * Sets ORDER BY condition in the query builder.
349
     * If you had previously ORDER BY expression defined,
350
     * calling this function will override previously set ORDER BY conditions.
351
     */
352
    orderBy(
353
        sort?: string | OrderByCondition,
354
        order: "ASC" | "DESC" = "ASC",
×
355
        nulls?: "NULLS FIRST" | "NULLS LAST",
356
    ): this {
357
        if (sort) {
×
358
            if (typeof sort === "object") {
×
359
                this.expressionMap.orderBys = sort as OrderByCondition
×
360
            } else {
361
                if (nulls) {
×
362
                    this.expressionMap.orderBys = {
×
363
                        [sort as string]: { order, nulls },
364
                    }
365
                } else {
366
                    this.expressionMap.orderBys = { [sort as string]: order }
×
367
                }
368
            }
369
        } else {
370
            this.expressionMap.orderBys = {}
×
371
        }
372
        return this
×
373
    }
374

375
    /**
376
     * Adds ORDER BY condition in the query builder.
377
     */
378
    addOrderBy(
379
        sort: string,
380
        order: "ASC" | "DESC" = "ASC",
×
381
        nulls?: "NULLS FIRST" | "NULLS LAST",
382
    ): this {
383
        if (nulls) {
×
384
            this.expressionMap.orderBys[sort] = { order, nulls }
×
385
        } else {
386
            this.expressionMap.orderBys[sort] = order
×
387
        }
388
        return this
×
389
    }
390

391
    /**
392
     * Sets LIMIT - maximum number of rows to be selected.
393
     */
394
    limit(limit?: number): this {
UNCOV
395
        this.expressionMap.limit = limit
×
UNCOV
396
        return this
×
397
    }
398

399
    /**
400
     * Indicates if entity must be updated after update operation.
401
     * This may produce extra query or use RETURNING / OUTPUT statement (depend on database).
402
     * Enabled by default.
403
     */
404
    whereEntity(entity: Entity | Entity[]): this {
UNCOV
405
        if (!this.expressionMap.mainAlias!.hasMetadata)
×
406
            throw new TypeORMError(
×
407
                `.whereEntity method can only be used on queries which update real entity table.`,
408
            )
409

UNCOV
410
        this.expressionMap.wheres = []
×
UNCOV
411
        const entities: Entity[] = Array.isArray(entity) ? entity : [entity]
×
UNCOV
412
        entities.forEach((entity) => {
×
413
            const entityIdMap =
UNCOV
414
                this.expressionMap.mainAlias!.metadata.getEntityIdMap(entity)
×
UNCOV
415
            if (!entityIdMap)
×
416
                throw new TypeORMError(
×
417
                    `Provided entity does not have ids set, cannot perform operation.`,
418
                )
419

UNCOV
420
            this.orWhereInIds(entityIdMap)
×
421
        })
422

UNCOV
423
        this.expressionMap.whereEntities = entities
×
UNCOV
424
        return this
×
425
    }
426

427
    /**
428
     * Indicates if entity must be updated after update operation.
429
     * This may produce extra query or use RETURNING / OUTPUT statement (depend on database).
430
     * Enabled by default.
431
     */
432
    updateEntity(enabled: boolean): this {
UNCOV
433
        this.expressionMap.updateEntity = enabled
×
UNCOV
434
        return this
×
435
    }
436

437
    // -------------------------------------------------------------------------
438
    // Protected Methods
439
    // -------------------------------------------------------------------------
440

441
    /**
442
     * Creates UPDATE express used to perform insert query.
443
     */
444
    protected createUpdateExpression() {
UNCOV
445
        const metadata = this.expressionMap.mainAlias!.hasMetadata
×
446
            ? this.expressionMap.mainAlias!.metadata
447
            : undefined
UNCOV
448
        if (!metadata)
×
449
            throw new TypeORMError(
×
450
                `Cannot get entity metadata for the given alias "${this.expressionMap.mainAlias}"`,
451
            )
UNCOV
452
        if (!metadata.deleteDateColumn) {
×
UNCOV
453
            throw new MissingDeleteDateColumnError(metadata)
×
454
        }
455

456
        // prepare columns and values to be updated
UNCOV
457
        const updateColumnAndValues: string[] = []
×
458

UNCOV
459
        switch (this.expressionMap.queryType) {
×
460
            case "soft-delete":
UNCOV
461
                updateColumnAndValues.push(
×
462
                    this.escape(metadata.deleteDateColumn.databaseName) +
463
                        " = CURRENT_TIMESTAMP",
464
                )
UNCOV
465
                break
×
466
            case "restore":
UNCOV
467
                updateColumnAndValues.push(
×
468
                    this.escape(metadata.deleteDateColumn.databaseName) +
469
                        " = NULL",
470
                )
UNCOV
471
                break
×
472
            default:
473
                throw new TypeORMError(
×
474
                    `The queryType must be "soft-delete" or "restore"`,
475
                )
476
        }
UNCOV
477
        if (metadata.versionColumn)
×
478
            updateColumnAndValues.push(
×
479
                this.escape(metadata.versionColumn.databaseName) +
480
                    " = " +
481
                    this.escape(metadata.versionColumn.databaseName) +
482
                    " + 1",
483
            )
UNCOV
484
        if (metadata.updateDateColumn)
×
UNCOV
485
            updateColumnAndValues.push(
×
486
                this.escape(metadata.updateDateColumn.databaseName) +
487
                    " = CURRENT_TIMESTAMP",
488
            ) // todo: fix issue with CURRENT_TIMESTAMP(6) being used, can "DEFAULT" be used?!
489

UNCOV
490
        if (updateColumnAndValues.length <= 0) {
×
491
            throw new UpdateValuesMissingError()
×
492
        }
493

494
        // get a table name and all column database names
UNCOV
495
        const whereExpression = this.createWhereExpression()
×
UNCOV
496
        const returningExpression = this.createReturningExpression("update")
×
497

UNCOV
498
        if (returningExpression === "") {
×
UNCOV
499
            return `UPDATE ${this.getTableName(
×
500
                this.getMainTableName(),
501
            )} SET ${updateColumnAndValues.join(", ")}${whereExpression}` // todo: how do we replace aliases in where to nothing?
502
        }
UNCOV
503
        if (this.connection.driver.options.type === "mssql") {
×
UNCOV
504
            return `UPDATE ${this.getTableName(
×
505
                this.getMainTableName(),
506
            )} SET ${updateColumnAndValues.join(
507
                ", ",
508
            )} OUTPUT ${returningExpression}${whereExpression}`
509
        }
UNCOV
510
        return `UPDATE ${this.getTableName(
×
511
            this.getMainTableName(),
512
        )} SET ${updateColumnAndValues.join(
513
            ", ",
514
        )}${whereExpression} RETURNING ${returningExpression}`
515
    }
516

517
    /**
518
     * Creates "ORDER BY" part of SQL query.
519
     */
520
    protected createOrderByExpression() {
UNCOV
521
        const orderBys = this.expressionMap.orderBys
×
UNCOV
522
        if (Object.keys(orderBys).length > 0)
×
523
            return (
×
524
                " ORDER BY " +
525
                Object.keys(orderBys)
526
                    .map((columnName) => {
527
                        if (typeof orderBys[columnName] === "string") {
×
528
                            return (
×
529
                                this.replacePropertyNames(columnName) +
530
                                " " +
531
                                orderBys[columnName]
532
                            )
533
                        } else {
534
                            return (
×
535
                                this.replacePropertyNames(columnName) +
536
                                " " +
537
                                (orderBys[columnName] as any).order +
538
                                " " +
539
                                (orderBys[columnName] as any).nulls
540
                            )
541
                        }
542
                    })
543
                    .join(", ")
544
            )
545

UNCOV
546
        return ""
×
547
    }
548

549
    /**
550
     * Creates "LIMIT" parts of SQL query.
551
     */
552
    protected createLimitExpression(): string {
UNCOV
553
        const limit: number | undefined = this.expressionMap.limit
×
554

UNCOV
555
        if (limit) {
×
UNCOV
556
            if (DriverUtils.isMySQLFamily(this.connection.driver)) {
×
UNCOV
557
                return " LIMIT " + limit
×
558
            } else {
UNCOV
559
                throw new LimitOnUpdateNotSupportedError()
×
560
            }
561
        }
562

UNCOV
563
        return ""
×
564
    }
565
}
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