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

typeorm / typeorm / 14796576772

02 May 2025 01:52PM UTC coverage: 45.367% (-30.9%) from 76.309%
14796576772

Pull #11434

github

web-flow
Merge ec4ce2d00 into fadad1a74
Pull Request #11434: feat: release PR releases using pkg.pr.new

5216 of 12761 branches covered (40.87%)

Branch coverage included in aggregate %.

11439 of 23951 relevant lines covered (47.76%)

15712.55 hits per line

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

66.34
/src/query-builder/SoftDeleteQueryBuilder.ts
1
import { QueryBuilder } from "./QueryBuilder"
4✔
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"
4✔
9
import { ReturningStatementNotSupportedError } from "../error/ReturningStatementNotSupportedError"
4✔
10
import { ReturningResultsEntityUpdator } from "./ReturningResultsEntityUpdator"
4✔
11
import { OrderByCondition } from "../find-options/OrderByCondition"
12
import { LimitOnUpdateNotSupportedError } from "../error/LimitOnUpdateNotSupportedError"
4✔
13
import { MissingDeleteDateColumnError } from "../error/MissingDeleteDateColumnError"
4✔
14
import { UpdateValuesMissingError } from "../error/UpdateValuesMissingError"
4✔
15
import { TypeORMError } from "../error"
4✔
16
import { DriverUtils } from "../driver/DriverUtils"
4✔
17
import { InstanceChecker } from "../util/InstanceChecker"
4✔
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>
4✔
23
    extends QueryBuilder<Entity>
24
    implements WhereExpressionBuilder
25
{
26
    readonly "@instanceof" = Symbol.for("SoftDeleteQueryBuilder")
148✔
27

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

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

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

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

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

62
        try {
148✔
63
            // start transaction if it was enabled
64
            if (
148!
65
                this.expressionMap.useTransaction === true &&
148!
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
73
            if (
148✔
74
                this.expressionMap.callListeners === true &&
196✔
75
                this.expressionMap.mainAlias!.hasMetadata
76
            ) {
77
                if (this.expressionMap.queryType === "soft-delete")
48✔
78
                    await queryRunner.broadcaster.broadcast(
28✔
79
                        "BeforeSoftRemove",
80
                        this.expressionMap.mainAlias!.metadata,
81
                    )
82
                else if (this.expressionMap.queryType === "restore")
20✔
83
                    await queryRunner.broadcaster.broadcast(
20✔
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 =
91
                new ReturningResultsEntityUpdator(
148✔
92
                    queryRunner,
93
                    this.expressionMap,
94
                )
95
            if (
148✔
96
                this.expressionMap.updateEntity === true &&
444✔
97
                this.expressionMap.mainAlias!.hasMetadata &&
98
                this.expressionMap.whereEntities.length > 0
99
            ) {
100
                this.expressionMap.extraReturningColumns =
88✔
101
                    returningResultsEntityUpdator.getSoftDeletionReturningColumns()
102
            }
103

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

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

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

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

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

142
            return updateResult
116✔
143
        } catch (error) {
144
            // rollback transaction if we started it
145
            if (transactionStartedByUs) {
32!
146
                try {
×
147
                    await queryRunner.rollbackTransaction()
×
148
                } catch (rollbackError) {}
149
            }
150
            throw error
32✔
151
        } finally {
152
            if (queryRunner !== this.queryRunner) {
148✔
153
                // means we created our own query runner
154
                await queryRunner.release()
48✔
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> {
171
        entityTarget = InstanceChecker.isEntitySchema(entityTarget)
136!
172
            ? entityTarget.options.name
173
            : entityTarget
174
        const mainAlias = this.createFromAlias(entityTarget, aliasName)
136✔
175
        this.expressionMap.setMainAlias(mainAlias)
136✔
176
        return this as any as SoftDeleteQueryBuilder<T>
136✔
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 {
194
        this.expressionMap.wheres = [] // don't move this block below since computeWhereParameter can add where expressions
52✔
195
        const condition = this.getWhereCondition(where)
52✔
196
        if (condition)
52✔
197
            this.expressionMap.wheres = [
52✔
198
                { type: "simple", condition: condition },
199
            ]
200
        if (parameters) this.setParameters(parameters)
52✔
201
        return this
52✔
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 {
238
        this.expressionMap.wheres.push({
88✔
239
            type: "or",
240
            condition: this.getWhereCondition(where),
241
        })
242
        if (parameters) this.setParameters(parameters)
88!
243
        return this
88✔
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 {
264
        return this.orWhere(this.getWhereInIdsCondition(ids))
88✔
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 {
395
        this.expressionMap.limit = limit
8✔
396
        return this
8✔
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 {
405
        if (!this.expressionMap.mainAlias!.hasMetadata)
88!
406
            throw new TypeORMError(
×
407
                `.whereEntity method can only be used on queries which update real entity table.`,
408
            )
409

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

420
            this.orWhereInIds(entityIdMap)
88✔
421
        })
422

423
        this.expressionMap.whereEntities = entities
88✔
424
        return this
88✔
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 {
433
        this.expressionMap.updateEntity = enabled
100✔
434
        return this
100✔
435
    }
436

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

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

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

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

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

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

498
        if (returningExpression === "") {
124✔
499
            return `UPDATE ${this.getTableName(
106✔
500
                this.getMainTableName(),
501
            )} SET ${updateColumnAndValues.join(", ")}${whereExpression}` // todo: how do we replace aliases in where to nothing?
502
        }
503
        if (this.connection.driver.options.type === "mssql") {
18!
504
            return `UPDATE ${this.getTableName(
×
505
                this.getMainTableName(),
506
            )} SET ${updateColumnAndValues.join(
507
                ", ",
508
            )} OUTPUT ${returningExpression}${whereExpression}`
509
        }
510
        return `UPDATE ${this.getTableName(
18✔
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() {
521
        const orderBys = this.expressionMap.orderBys
124✔
522
        if (Object.keys(orderBys).length > 0)
124!
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

546
        return ""
124✔
547
    }
548

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

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

563
        return ""
116✔
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