- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
- 31. API with NestJS #31. Two-factor authentication
- 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
- 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
- 34. API with NestJS #34. Handling CPU-intensive tasks with queues
- 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
- 36. API with NestJS #36. Introduction to Stripe with React
- 37. API with NestJS #37. Using Stripe to save credit cards for future use
- 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
- 39. API with NestJS #39. Reacting to Stripe events with webhooks
- 40. API with NestJS #40. Confirming the email address
- 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
- 42. API with NestJS #42. Authenticating users with Google
- 43. API with NestJS #43. Introduction to MongoDB
- 44. API with NestJS #44. Implementing relationships with MongoDB
- 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
- 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
- 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
- 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
- 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
- 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
- 51. API with NestJS #51. Health checks with Terminus and Datadog
- 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
- 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
- 54. API with NestJS #54. Storing files inside a PostgreSQL database
- 55. API with NestJS #55. Uploading files to the server
- 56. API with NestJS #56. Authorization with roles and claims
- 57. API with NestJS #57. Composing classes with the mixin pattern
- 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
- 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
- 60. API with NestJS #60. The OpenAPI specification and Swagger
- 61. API with NestJS #61. Dealing with circular dependencies
- 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
- 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
- 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
- 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
- 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
- 67. API with NestJS #67. Migrating to TypeORM 0.3
- 68. API with NestJS #68. Interacting with the application through REPL
- 69. API with NestJS #69. Database migrations with TypeORM
- 70. API with NestJS #70. Defining dynamic modules
- 71. API with NestJS #71. Introduction to feature flags
- 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
- 73. API with NestJS #73. One-to-one relationships with raw SQL queries
- 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
- 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
- 76. API with NestJS #76. Working with transactions using raw SQL queries
- 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
- 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
- 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
- 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
- 81. API with NestJS #81. Soft deletes with raw SQL queries
- 82. API with NestJS #82. Introduction to indexes with raw SQL queries
- 83. API with NestJS #83. Text search with tsvector and raw SQL
- 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
- 85. API with NestJS #85. Defining constraints with raw SQL
- 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
- 87. API with NestJS #87. Writing unit tests in a project with raw SQL
- 88. API with NestJS #88. Testing a project with raw SQL using integration tests
- 89. API with NestJS #89. Replacing Express with Fastify
- 90. API with NestJS #90. Using various types of SQL joins
- 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
- 92. API with NestJS #92. Increasing the developer experience with Docker Compose
- 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
- 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer
- 95. API with NestJS #95. CI/CD with Amazon ECS and GitHub Actions
- 96. API with NestJS #96. Running unit tests with CI/CD and GitHub Actions
- 97. API with NestJS #97. Introduction to managing logs with Amazon CloudWatch
- 98. API with NestJS #98. Health checks with Terminus and Amazon ECS
- 99. API with NestJS #99. Scaling the number of application instances with Amazon ECS
- 100. API with NestJS #100. The HTTPS protocol with Route 53 and AWS Certificate Manager
- 101. API with NestJS #101. Managing sensitive data using the AWS Secrets Manager
- 102. API with NestJS #102. Writing unit tests with Prisma
- 103. API with NestJS #103. Integration tests with Prisma
- 104. API with NestJS #104. Writing transactions with Prisma
- 105. API with NestJS #105. Implementing soft deletes with Prisma and middleware
- 106. API with NestJS #106. Improving performance through indexes with Prisma
- 107. API with NestJS #107. Offset and keyset pagination with Prisma
- 108. API with NestJS #108. Date and time with Prisma and PostgreSQL
- 109. API with NestJS #109. Arrays with PostgreSQL and Prisma
- 110. API with NestJS #110. Managing JSON data with PostgreSQL and Prisma
- 111. API with NestJS #111. Constraints with PostgreSQL and Prisma
- 112. API with NestJS #112. Serializing the response with Prisma
- 113. API with NestJS #113. Logging with Prisma
- 114. API with NestJS #114. Modifying data using PUT and PATCH methods with Prisma
- 115. API with NestJS #115. Database migrations with Prisma
- 116. API with NestJS #116. REST API versioning
- 117. API with NestJS #117. CORS – Cross-Origin Resource Sharing
- 118. API with NestJS #118. Uploading and streaming videos
- 119. API with NestJS #119. Type-safe SQL queries with Kysely and PostgreSQL
- 120. API with NestJS #120. One-to-one relationships with the Kysely query builder
- 121. API with NestJS #121. Many-to-one relationships with PostgreSQL and Kysely
In this article, we look into the filters feature and implement soft deletes. With them, we can mark an entity as deleted without removing it from the database permanently.
You can get the code from this article in this repository.
Introducing MikroORM filters
We can provide various filters when querying data using functions such as find and findOne. For example, let’s query only short posts using the LENGTH function built into PostgreSQL. To do that, we also need $lt to create a smart query condition.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository, expr } from "@mikro-orm/core"; import PostEntity from './post.entity'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} getShortPosts() { return this.postRepository.find( { [expr('length(content)')]: { $lt: '100', }, }, { populate: ['author', 'author.address', 'categories'], }, ); } // ... } |
Above, we use the expr helper to bypass a strict type check in the FilterQuery interface built into MikroORM. We need it, because there is no column named length(content).
Instead of the above approach, we can use the @Filter() decorator to predefine filter criteria and attach them to a class.
post.entity.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Entity, Property, PrimaryKey, Filter } from '@mikro-orm/core'; @Entity() @Filter({ name: 'short', cond: { 'length(content)': { $lt: 100 } } }) class PostEntity { @PrimaryKey() id: number; @Property() title: string; @Property() content: string; // ... } export default PostEntity; |
The cond property works in a similar way to queries in functions such as find and findOne.
Thanks to attaching our short filter, we can now use it in our queries.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from "@mikro-orm/core"; import PostEntity from './post.entity'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} getPosts(getOnlyShortPosts?: boolean) { return this.postRepository.findAll({ populate: ['author', 'author.address', 'categories'], filters: { short: getOnlyShortPosts } }); } } |
Adding arguments
Besides simple boolean-based filters, we can pass additional arguments through an object. Let’s make our previous filter more generic by adding the option of specifying the maximum content length.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Filter({ name: 'maximumContentLength', cond: ({ maximumContentLength }: { maximumContentLength?: number }) => { if (maximumContentLength === undefined) { return {}; } return { 'length(content)': { $lt: maximumContentLength, }, }; }, }) |
Thanks to the above, we can now pass the content length when querying posts.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import PostEntity from './post.entity'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} getPosts(maximumContentLength?: number) { return this.postRepository.findAll({ populate: ['author', 'author.address', 'categories'], filters: { maximumContentLength: { maximumContentLength }, }, }); } // ... } |
Enabling filters by default
So far, we’ve been creating filters we can use through functions such as find and findOne. Instead of that, we can define filters toggled by default. For example, let’s create a filter that skips posts that have an empty string for content.
1 2 3 4 5 6 7 8 9 |
@Filter({ name: 'skipEmptyPosts', cond: () => ({ 'length(content)': { $gt: 0 } }), default: true }) |
Even though our skipEmptyPosts filter works by default, we can turn it off for a particular query if we need to.
1 2 3 4 5 6 7 8 |
getPosts() { return this.postRepository.findAll({ populate: ['author', 'author.address', 'categories'], filters: { skipEmptyPosts: false } }); } |
Implementing soft deletes
The idea behind soft deletes is that instead of permanently removing records from the database, we only mark them as deleted. Thanks to doing so, we can always restore the data deleted previously. In addition, it can help us increase the user experience of our application by creating an undo button, for example.
1 2 3 4 5 6 |
CREATE TABLE posts ( id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text, content text, is_deleted boolean DEFAULT false ) |
If you want to know more about the identity column above, check out Serial type versus identity columns in PostgreSQL and TypeORM
If we want to mark a post as deleted, we need to change is_deleted to true.
1 2 3 |
UPDATE posts SET is_deleted = true WHERE id = 1 |
A disadvantage of soft deletes is that we always need to consider the is_deleted column when performing various other queries.
Fortunately, MikroORM will be able to do this for us. Still, it takes some additional computing power to filter out deleted entities.
1 2 |
SELECT * from posts WHERE is_deleted = false |
There is also an important catch with columns marked as unique. Let’s imagine the following scenario:
- create a posts table where every title needs to be unique,
- insert a post with a given title,
- delete the above post with a soft delete,
- try to insert a post with the same title again.
Doing the above would result in an error, unfortunately.
Soft deletes with MikroORM
To use MikroORM to achieve soft deletes, we can create a custom decorator with a filter. While on it, we can create a deletedAt column with the deletion date instead of a boolean flag.
If you want to know more about handling dates with PostgrteSQL, check out Managing date and time with PostgreSQL and TypeORM
withSoftDelete.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Filter } from '@mikro-orm/core'; const WithSoftDelete = (): ClassDecorator => { return Filter({ name: 'softDelete', cond: { deletedAt: null, }, default: true, }); }; export default WithSoftDelete; |
Thanks to writing deletedAt: null above, we filter out all posts marked as deleted.
post.entity.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { Entity, Property, PrimaryKey, Index, } from '@mikro-orm/core'; import WithSoftDelete from '../utils/withSoftDelete'; @Entity() @WithSoftDelete() class PostEntity { @PrimaryKey() id: number; @Property() title: string; @Property() content: string; @Index() @Property({ nullable: true, type: 'timestamptz' }) deletedAt?: Date; // ... } export default PostEntity; |
Please notice that we use the @Index() decorator above on the deletedAt property. Thanks to doing that, we improve the performance of various queries. Since many of our queries will include checking if a post is deleted, creating an index might be a good idea.
If you want to know more about indexes, check out API with NestJS #14. Improving performance of our Postgres database with indexes
We also need to add a new method to our service that sets the value of the deletedAt column.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import PostEntity from './post.entity'; import PostNotFoundException from './exceptions/postNotFound.exception'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} async getPostById(id: number) { const post = await this.postRepository.findOne({ id, }); if (!post) { throw new PostNotFoundException(id); } return post; } async softDeletePost(id: number) { const existingPost = await this.getPostById(id); existingPost.deletedAt = new Date(); await this.postRepository.persistAndFlush(existingPost); } // ... } |
Fetching deleted posts
We can modify our filter to accept arguments if we need to fetch the deleted posts or all posts.
withSoftDelete.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Filter } from '@mikro-orm/core'; interface FilterArguments { getAll?: boolean; getOnlyDeleted?: boolean; } const WithSoftDelete = (): ClassDecorator => { return Filter({ name: 'softDelete', cond: ({ getAll, getOnlyDeleted }: FilterArguments = {}) => { if (getAll) { return {}; } if (getOnlyDeleted) { return { deletedAt: { $ne: null, }, }; } return { deletedAt: null, }; }, default: true, }); }; export default WithSoftDelete; |
We need to pass an additional argument to the filter to get a deleted post.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import PostEntity from './post.entity'; import PostNotFoundException from './exceptions/postNotFound.exception'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} async getDeletedPost(id: number) { const post = await this.postRepository.findOne( { id, }, { filters: { softDelete: { getOnlyDeleted: true, }, }, }, ); if (!post) { throw new PostNotFoundException(id); } return post; } // ... } |
Restoring deleted posts
The getDeletedPost method we’ve created above can come in handy when restoring a deleted post. We need to set the deletedAt column back to null to do that.
posts.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import PostEntity from './post.entity'; import PostNotFoundException from './exceptions/postNotFound.exception'; @Injectable() export class PostsService { constructor( @InjectRepository(PostEntity) private readonly postRepository: EntityRepository<PostEntity>, ) {} async getDeletedPost(id: number) { const post = await this.postRepository.findOne( { id, }, { filters: { softDelete: { getOnlyDeleted: true, }, }, }, ); if (!post) { throw new PostNotFoundException(id); } return post; } async restorePost(id: number) { const existingPost = await this.getDeletedPost(id); existingPost.deletedAt = null; await this.postRepository.persistAndFlush(existingPost); return existingPost; } // ... } |
Summary
In this article, we’ve gone through the feature of filters in MikroORM. While we might prefer to write the query-related logic in a service, creating reusable filters might be a useful pattern. Besides that, we’ve learned about what soft delete is and what are its advantages and disadvantages. We’ve also created a reusable filter that helps us implement soft deletes with MikroORM.
You could add a global filter in the module configuration to get the same behavior of filtering all soft deleted rows. I think the getOnlyDeleted filter could be added too. Anyway, this approach or the one from the post doesn’t work with pivot tables.
For example this
where userRefreshTokens is a relation of @OneToMany would execute this query
I need to add the filter manually to get the same behavior with relationships