- 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
- 122. API with NestJS #122. Many-to-many relationships with Kysely and PostgreSQL
- 123. API with NestJS #123. SQL transactions with Kysely
- 124. API with NestJS #124. Handling SQL constraints with Kysely
- 125. API with NestJS #125. Offset and keyset pagination with Kysely
- 126. API with NestJS #126. Improving the database performance with indexes and Kysely
- 127. API with NestJS #127. Arrays with PostgreSQL and Kysely
- 128. API with NestJS #128. Managing JSON data with PostgreSQL and Kysely
- 129. API with NestJS #129. Implementing soft deletes with SQL and Kysely
- 130. API with NestJS #130. Avoiding storing sensitive information in API logs
- 131. API with NestJS #131. Unit tests with PostgreSQL and Kysely
- 132. API with NestJS #132. Handling date and time in PostgreSQL with Kysely
- 133. API with NestJS #133. Introducing database normalization with PostgreSQL and Prisma
- 134. API with NestJS #134. Aggregating statistics with PostgreSQL and Prisma
- 135. API with NestJS #135. Referential actions and foreign keys in PostgreSQL with Prisma
- 136. API with NestJS #136. Raw SQL queries with Prisma and PostgreSQL range types
- 137. API with NestJS #137. Recursive relationships with Prisma and PostgreSQL
- 138. API with NestJS #138. Filtering records with Prisma
- 139. API with NestJS #139. Using UUID as primary keys with Prisma and PostgreSQL
- 140. API with NestJS #140. Using multiple PostgreSQL schemas with Prisma
- 141. API with NestJS #141. Getting distinct records with Prisma and PostgreSQL
- 142. API with NestJS #142. A video chat with WebRTC and React
- 143. API with NestJS #143. Optimizing queries with views using PostgreSQL and Kysely
- 144. API with NestJS #144. Creating CLI applications with the Nest Commander
- 145. API with NestJS #145. Securing applications with Helmet
- 146. API with NestJS #146. Polymorphic associations with PostgreSQL and Prisma
- 147. API with NestJS #147. The data types to store money with PostgreSQL and Prisma
- 148. API with NestJS #148. Understanding the injection scopes
- 149. API with NestJS #149. Introduction to the Drizzle ORM with PostgreSQL
- 150. API with NestJS #150. One-to-one relationships with the Drizzle ORM
- 151. API with NestJS #151. Implementing many-to-one relationships with Drizzle ORM
- 152. API with NestJS #152. SQL constraints with the Drizzle ORM
- 153. API with NestJS #153. SQL transactions with the Drizzle ORM
- 154. API with NestJS #154. Many-to-many relationships with Drizzle ORM and PostgreSQL
- 155. API with NestJS #155. Offset and keyset pagination with the Drizzle ORM
- 156. API with NestJS #156. Arrays with PostgreSQL and the Drizzle ORM
- 157. API with NestJS #157. Handling JSON data with PostgreSQL and the Drizzle ORM
- 158. API with NestJS #158. Soft deletes with the Drizzle ORM
- 159. API with NestJS #159. Date and time with PostgreSQL and the Drizzle ORM
- 160. API with NestJS #160. Using views with the Drizzle ORM and PostgreSQL
- 161. API with NestJS #161. Generated columns with the Drizzle ORM and PostgreSQL
- 162. API with NestJS #162. Identity columns with the Drizzle ORM and PostgreSQL
- 163. API with NestJS #163. Full-text search with the Drizzle ORM and PostgreSQL
- 164. API with NestJS #164. Improving the performance with indexes using Drizzle ORM
- 165. API with NestJS #165. Time intervals with the Drizzle ORM and PostgreSQL
- 166. API with NestJS #166. Logging with the Drizzle ORM
Designing relationships is one of the crucial aspects of working with SQL databases. In this article, we continue using Kysely with NestJS and implement many-to-one relationships.
Check out this repository if you want to see the full code from this article.
Introducing the many-to-one relationship
When implementing the many-to-one relationship, a row from the first table is connected to multiple rows in the second table. What’s essential, a row from the second table can connect to just one row from the first table.
An example is an article with a single author, while the user can be an author of many articles. A way to implement it is to save the author’s id in the articles table as a foreign key. A foreign key is a value that matches a column from a different table.
Whenever we create a foreign key in our database, PostgreSQL defines the foreign key constraint to ensure the consistency of our data. Thanks to that, it prevents us from having an author_id value that refers to a user that does not exist. We can’t:
- create an article and provide the author_id that points to a user that cannot be found in the users table,
- update an existing article and change the author_id to match a user that does not exist,
- delete a user with an id used in the
author_id column
- we would have to delete the article first or change its author
- alternatively, we could use the CASCADE option to force PostgreSQL to delete all articles the deleted user is an author of
Defining the many-to-one relationship with Kysely
In one of the previous parts of this series, we learned how to write SQL migrations when using Kysely. This time, we want to add the author_id column that is not nullable. Unfortunately, we might already have some articles in our database, and adding a new non-nullable column without a default value would cause an error.
1 2 |
ALTER TABLE articles ADD COLUMN author_id int REFERENCES users(id) NOT NULL |
ERROR: column “author_id” of relation “articles” contains null values
To solve the above problem, we can provide a default value for the author_id column. To do that, we need to have a default user. Let’s add a seed file to our migrations directory. Creating seed files is a way to populate our database with initial data.
Adding the seed file
First, let’s add the email and password of the admin to the environment variables.
.env
1 2 |
ADMIN_EMAIL=admin@admin.com ADMIN_PASSWORD=strongPassword |
Now we can add the seed file to our migrations.
20230817223154_insert_admin.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 { config } from 'dotenv'; import * as bcrypt from 'bcrypt'; import { Database } from '../database/database'; import { ConfigService } from '@nestjs/config'; import { EnvironmentVariables } from '../types/environmentVariables'; import { Migration } from 'kysely'; config(); const configService = new ConfigService<EnvironmentVariables>(); export const up: Migration['up'] = async (database) => { const email = configService.get('ADMIN_EMAIL'); const password = configService.get('ADMIN_PASSWORD'); const hashedPassword = await bcrypt.hash(password, 10); await database .insertInto('users') .values({ email, password: hashedPassword, name: 'Admin', }) .execute(); }; export const down: Migration['up'] = async (database) => { const email = configService.get('ADMIN_EMAIL'); await database.deleteFrom('users').where('email', '=', email).execute(); }; |
Creating the migration
When writing the migration file that adds the author_id column, we can implement the following approach:
- get the id of the admin,
- add the author_id column as nullable,
- set the value in the author_id column for articles that don’t have it,
- make the author_id column non-nullable.
20230817230950_add_author_id.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 42 43 44 45 46 47 48 |
import { Kysely, Migration } from 'kysely'; import { config } from 'dotenv'; import { ConfigService } from '@nestjs/config'; import { EnvironmentVariables } from '../types/environmentVariables'; config(); const configService = new ConfigService<EnvironmentVariables>(); export const up: Migration['up'] = async (database) => { const email = configService.get('ADMIN_EMAIL'); const adminDatabaseResponse = await database .selectFrom('users') .where('email', '=', email) .selectAll() .executeTakeFirstOrThrow(); const adminId = adminDatabaseResponse.id; await database.schema .alterTable('articles') .addColumn('author_id', 'integer', (column) => { return column.references('users.id'); }) .execute(); await database .updateTable('articles') .set({ author_id: adminId, }) .execute(); await database.schema .alterTable('articles') .alterColumn('author_id', (column) => { return column.setNotNull(); }) .execute(); }; export async function down(database: Kysely<unknown>): Promise<void> { await database.schema .alterTable('articles') .dropColumn('author_id') .execute(); } |
Many-to-one vs one-to-one
In the previous part of this series, we’ve covered the one-to-one relationship. When writing the migration, we’ve run the following query:
1 2 3 4 5 6 |
await database.schema .alterTable('users') .addColumn('address_id', 'integer', (column) => { return column.unique().references('addresses.id'); }) .execute(); |
Adding the unique constraint ensures that a particular address belongs to only one user.
On the contrary, when adding the author_id column, we ran the query without adding the unique constraint:
1 2 3 4 5 6 |
await database.schema .alterTable('articles') .addColumn('author_id', 'integer', (column) => { return column.references('users.id'); }) .execute(); |
Thanks to the above approach, multiple articles can share the same author.
Creating an article with the author
The next thing we need to do is to modify the TypeScript definition of our articles table.
articlesTable.ts
1 2 3 4 5 6 7 8 |
import { Generated } from 'kysely'; export interface ArticlesTable { id: Generated<number>; title: string; article_content: string; author_id: number; } |
Let’s also add the author’s id to the article’s model.
articles.model.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
interface ArticleModelData { id: number; title: string; article_content: string; author_id: number; } export class Article { id: number; title: string; content: string; authorId: number; constructor({ id, title, article_content, author_id }: ArticleModelData) { this.id = id; this.title = title; this.content = article_content; this.authorId = author_id; } } |
We must also handle the author_id column when inserting the article into our database.
articles.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 |
import { Database } from '../database/database'; import { Article } from './article.model'; import { Injectable } from '@nestjs/common'; import ArticleDto from './dto/article.dto'; @Injectable() export class ArticlesRepository { constructor(private readonly database: Database) {} async create(data: ArticleDto, authorId: number) { const databaseResponse = await this.database .insertInto('articles') .values({ title: data.title, article_content: data.content, author_id: authorId, }) .returningAll() .executeTakeFirstOrThrow(); return new Article(databaseResponse); } // ... } |
When figuring out who is the author of the new article, we don’t expect the information to be provided directly through the body of the POST request. Instead, we get this information by decoding the JWT token.
If you want to read more about JWT tokens and authentication, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
articles.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common'; import ArticleDto from './dto/article.dto'; import { ArticlesService } from './articles.service'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import { RequestWithUser } from '../authentication/requestWithUser.interface'; @Controller('articles') export class ArticlesController { constructor(private readonly articlesService: ArticlesService) {} @Post() @UseGuards(JwtAuthenticationGuard) create(@Body() data: ArticleDto, @Req() request: RequestWithUser) { return this.articlesService.create(data, request.user.id); } // ... } |
Fetching articles of a particular user
We can query the articles written by a particular author using the where function.
articles.repository.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Database } from '../database/database'; import { Article } from './article.model'; import { Injectable } from '@nestjs/common'; @Injectable() export class ArticlesRepository { constructor(private readonly database: Database) {} async getByAuthorId(authorId: number) { const databaseResponse = await this.database .selectFrom('articles') .where('author_id', '=', authorId) .selectAll() .execute(); return databaseResponse.map((articleData) => new Article(articleData)); } // ... } |
Let’s use a different method from our repository based on whether the author’s id is provided.
articles.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Injectable } from '@nestjs/common'; import { ArticlesRepository } from './articles.repository'; @Injectable() export class ArticlesService { constructor(private readonly articlesRepository: ArticlesRepository) {} getAll(authorId?: number) { if (authorId) { return this.articlesRepository.getByAuthorId(authorId); } return this.articlesRepository.getAll(); } // ... } |
A good way to use the above feature through our REST API is with a query parameter. Let’s define a class that validates if it is provided using the correct format.
getArticlesByAuthorQuery.service.ts
1 2 3 4 5 6 7 8 9 10 |
import { Transform } from 'class-transformer'; import { IsNumber, IsOptional, Min } from 'class-validator'; export class GetArticlesByAuthorQuery { @IsNumber() @Min(1) @IsOptional() @Transform(({ value }) => Number(value)) authorId?: number; } |
We can now use the above class in our controller.
articles.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { Controller, Get, Query } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { GetArticlesByAuthorQuery } from './getArticlesByAuthorQuery'; @Controller('articles') export class ArticlesController { constructor(private readonly articlesService: ArticlesService) {} @Get() getAll(@Query() { authorId }: GetArticlesByAuthorQuery) { return this.articlesService.getAll(authorId); } // ... } |
Combining the data from both tables
It is common to want to combine the data from more than one table. Let’s create a model containing detailed information about the article and its author.
articleWithAuthor.model.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 |
import { Article, ArticleModelData } from './article.model'; import { User } from '../users/user.model'; interface ArticleWithAuthorModelData extends ArticleModelData { user_id: number; user_email: string; user_name: string; user_password: string; address_id: number | null; address_street: string | null; address_city: string | null; address_country: string | null; } export class ArticleWithAuthor extends Article { author: User; constructor(articleData: ArticleWithAuthorModelData) { super(articleData); this.author = new User({ id: articleData.user_id, email: articleData.user_email, name: articleData.user_name, password: articleData.user_password, address_city: articleData.address_city, address_country: articleData.address_country, address_street: articleData.address_street, address_id: articleData.address_id, }); } } |
We need to perform a join to fetch the author’s data together with the article.
1 2 3 4 5 6 |
SELECT articles.id AS id, articles.title AS title, articles.article_content AS article_content, articles.author_id as author_id, users.id AS user_id, users.email AS user_email, users.name AS user_name, users.password AS user_password FROM articles JOIN users ON articles.author_id = users.id WHERE articles.id=$1 |
The default type of join is the inner join. It returns records that have matching values in both tables. Since every article requires an author, it works as expected.
In the previous article, we implemented the outer join when fetching the user together with the address since the address is optional. Outer joins preserve the rows that don’t have matching values in both tables.
We must perform two joins to query the article, author, and possible address.
articles.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 |
import { Database } from '../database/database'; import { Injectable } from '@nestjs/common'; import { ArticleWithAuthorModel } from './articleWithAuthor.model'; @Injectable() export class ArticlesRepository { constructor(private readonly database: Database) {} async getWithAuthor(id: number) { const databaseResponse = await this.database .selectFrom('articles') .where('articles.id', '=', id) .innerJoin('users', 'users.id', 'articles.author_id') .leftJoin('addresses', 'addresses.id', 'users.address_id') .select([ 'articles.id as id', 'articles.article_content as article_content', 'articles.title as title', 'articles.author_id as author_id', 'users.id as user_id', 'users.email as user_email', 'users.name as user_name', 'users.password as user_password', 'addresses.id as address_id', 'addresses.city as address_city', 'addresses.street as address_street', 'addresses.country as address_country', ]) .executeTakeFirst(); if (databaseResponse) { return new ArticleWithAuthorModel(databaseResponse); } } // ... } |
Summary
In this article, we’ve explained the one-to-many relationship in SQL and implemented it using Kysely and NestJS. When doing that, we had to make a SQL query that used more than one join. We also learned how to write a migration that adds a new non-nullable column and how to avoid errors when running it on an existing database. There is still more to cover regarding relationships with PostgreSQL and Kysely, so stay tuned!