This is a premium alert message you can set from Layout! Get Now!

Implement in-app notifications with NestJS, MySQL, and Firebase

0

In-app alerts are a great way to provide your application users with real-time information. Firebase Cloud Messaging (FCM) is a cross-platform messaging system that is a great tool for sending push notifications to your client app.

In this article, we’ll cover setting up Firebase Cloud Messaging in a NestJS project. We’ll use a MySQL database to store and retrieve notification data and send push notifications to users depending on the database information.

Jump ahead:

Prerequisites

Before getting started, be sure that you have the following:

  • NestJS CLI globally installed
  • A Firebase project with the Cloud Messaging service enabled

To follow along, you can check out the GitHub repo. Let’s get started!

Setting up Firebase Cloud Messaging

To get started with FCM in NestJS, you’ll need to set up a Firebase project and enable the FCM API. To do so, follow the instructions from the Firebase documentation.

Once you’ve set up your Firebase project, you’ll need to create a service account and download the private key file, a JSON file that contains the credentials required to authenticate the Firebase Admin SDK.

Move the private key file to the root of your application; for this tutorial, we’ll rename the file firebase-admin-sdk.json. Install the firebase-admin package with the command below:

npm install --save firebase-admin

Create NestJS user and notification resources

In a system that uses push notifications, user resources refer to the information that is stored and managed for each individual user; this may include information like the user’s device type, push notification preferences, and contact information. Typically, this information is used to tailor the content and delivery of push notifications to each user based on their preferences and history.

On the other hand, notification resources refer to the logic and functionality that is used to enable, disable, and send push notifications. This may include the ability to schedule notifications, create and manage notification groups, and customize the content and appearance of notifications. This may also include tracking the delivery and engagement of notifications, for example, by using open rates, click-through rates, and more.

We’ll generate these new NestJS resources with the respective code snippets below:

 nest g res notification --no-spec
 nest g res users --no-spec

Setting up MySQL

Before working on our notification resource, let’s set up our MySQL database to store the notification data. First, install the MySQL2 and TypeORM packages:

npm install --save mysql2 typeorm @nestjs/typeorm @nestjs/config

Now, let’s update the app.module.ts:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppService } from './app.service';
import { Notification } from './notification/entities/notification.entity';
import { NotificationModule } from './notification/notification.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: "mysql",
      host: process.env.DB_HOST,
      port: 3306,
      username: procees.env.DB_NAME,
      password: process.env.DB_PASSWORD,
      database: "in_app_notification",
      entities: [Notification],
      synchronize: true,
  }),
    NotificationModule
  ],
  controllers: [],
  providers: [AppService],
})

export class AppModule {}

First, TypeOrmModule.forRoot({...}) imports the TypeOrmModule. Then, it uses the .forRoot() method to provide the module with a configuration object that sets up the connection to a MySQL database, specifying the host, port, username, password, database name, the type of database, the entities to be used for the table, and the synchronization status.

We define environment variables in a .env file located in the root of your application. Keep in mind that in a production environment, it is advisable to use migrations and set synchronization to false.

User resources

We’ll use TypeOrmModule to set up the MySQL connection and to create the User entity. Add the following code to user.module.ts:

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { NotificationModule } from 'src/notification/notification.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    NotificationModule
  ],
  controllers: [UsersController],
  providers: [UsersService]
})

export class UsersModule {}

Create User entity

Entities help us to define the database table schema; by default, the User entity defines the ID, email, username, and status as ACTIVE:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'users' })
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  username: string;

  @Column({
    default: 'ACTIVE',
  })
  status: string;
}

For now, this will all be stored in the user module. We’ll revisit this module later to utilize the push notifications.

Notification resources

In the module, we’ll use TypeOrmModule to set up the MySQL connection, create the Notification entity, and export the service. Add the code below to notification.module.ts:

import { Module } from '@nestjs/common';
import { NotificationService } from './notification.service';
import { NotificationController } from './notification.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Notifications } from './entities/notification.entity';
import { NotificationToken } from './entities/notification-token.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([Notifications, NotificationToken])
  ],
  controllers: [NotificationController],
  providers: [NotificationService],
  exports: [NotificationService]
})

export class NotificationModule {}

Create notifications and NotificationToken entity

Add the following code to notification.entity.ts:

import { Entity, Column, PrimaryGeneratedColumn, JoinColumn, ManyToOne } from 'typeorm';
import { NotificationToken } from './notification-token.entity';

@Entity({ name: 'notifications' })
export class Notifications {
  @PrimaryGeneratedColumn()
  id: number;

  @JoinColumn({ name: 'notification_token_id', referencedColumnName: 'id' })
  @ManyToOne(() => NotificationToken)
  notification_token: NotificationToken;

  @Column()
  title: string;

  @Column({ type: 'longtext', nullable: true })
  body: any;

  @Column()
  created_by: string;

  @Column({
    default: 'ACTIVE',
  })
  status: string;
}

The Notification entity has the following columns: id, title, body, status, and created_by.

The id column is the primary key and is generated automatically by the database. The notification_token has a ManyToOne relationship with the NoticationToken table, and the other columns store the data for the notifications.

Next, add the following code to the notification-token.entity.ts file:

import { User } from 'src/users/entities/user.entity';
import { Entity, Column, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'notification_tokens' })
export class NotificationToken {
  @PrimaryGeneratedColumn()
  id: number;

  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
  @ManyToOne(() => User)
  user: User;

  @Column()
  device_type: string;

  @Column()
  notification_token: string;

  @Column({
    default: 'ACTIVE',
  })
  status: string;
}

The NotificationToken entity has the id, user, device_type, status, and notification_token columns; notification_token is a unique string generated on registered FCM devices.

The id column is the primary key and is generated automatically by the database. The notification_token has a ManyToOne relationship with the User table, while the other columns store the data for the notification.

FCM notification token use case on the server

A mobile application or browser registers itself with FCM, and FCM then gives that device a unique token. The device sends this token to our NestJS server, and we store that FCM token in our database.

When the user gets the push notification, the unique token registered to the user device will be responsible for sending push notifications using the Firebase Admin SDK.

notification.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Notifications } from './entities/notification.entity';
import { Repository } from 'typeorm';
import * as firebase from 'firebase-admin';
import * as path from 'path';
import { NotificationToken } from './entities/notification-token.entity';
import { NotificationDto } from './dto/create-notification.dto';
import { UpdateNotificationDto } from './dto/update-notification.dto';

firebase.initializeApp({
  credential: firebase.credential.cert(
    path.join(__dirname, '..', '..', 'firebase-adminsdk.json'),
  ),
});

@Injectable()
export class NotificationService {
  constructor(
    @InjectRepository(Notifications) private readonly notificationsRepo: Repository<Notifications>,
    @InjectRepository(NotificationToken) private readonly notificationTokenRepo: Repository<NotificationToken>,
  ) {}

  acceptPushNotification = async (
    user: any,
    notification_dto: NotificationDto ,
  ): Promise<NotificationToken> => {};

  disablePushNotification = async (
    user: any,
    update_dto: UpdateNotificationDto,
  ): Promise<void> => {};

  getNotifications = async (): Promise<any> => {};

  sendPush = async (user: any, title: string, body: string): Promise<void> => {};
}

The code above first imports the necessary modules and dependencies, including the NestJS Injectable and InjectRepository decorators, the TypeORM Repository, Notifications, and NotificationToken entities, and the Firebase firebase-admin module. It then initializes the Firebase app using a service account key stored in a firebase-adminsdk.json file.

The NotificationService class is then defined and decorated with the Injectable decorator. It has three repositories as private properties, notificationsRepo and notificationTokenRepo for the Notifications and NotificationToken entities, respectively.

The NotificationService class has four methods:

acceptPushNotification

acceptPushNotification = async (
    user: any,
    notification_dto: NotificationDto ,
  ): Promise<NotificationToken> => {
    await this.notificationTokenRepo.update(
      { user: { id: user.id } },
      {
        status: 'INACTIVE',
      },
    );
    // save to db
    const notification_token = await this.notificationTokenRepo.save({
      user: user,
      device_type: notification_dto.device_type,
      notification_token: notification_dto.notification_token,
      status: 'ACTIVE',
    });
    return notification_token;
  };

acceptPushNotification updates the status of all existing notification tokens for a user to INACTIVE using the notificationTokenRepo.update method. It creates a new notification token object with the provided user object, the token, and device type, setting the status to ACTIVE.

Finally, it saves the new notification token to the database using the notificationTokenRepo.save method.

disablePushNotification

disablePushNotification = async (
    user: any,
    update_dto: UpdateNotificationDto,
  ): Promise<void> => {
    try {
      await this.notificationTokenRepo.update(
        { user: { id: user.id }, device_type: update_dto.device_type },
        {
          status: 'INACTIVE',
        },
      );
    } catch (error) {
      return error;
    }
  };

disablePushNotification updates the status of a specific notification token for a user to INACTIVE using the notificationTokenRepo.update method, based on the user’s ID and device type provided in the update DTO.

getNotifications

getNotifications = async (): Promise<any> => {
    return await this.notificationsRepo.find();
  };

getNotifications retrieves all notifications from the database using the notificationsRepo.find() method.

sendPush

sendPush = async (user: any, title: string, body: string): Promise<void> => {
    try {
      const notification = await this.notificationTokenRepo.findOne({
        where: { user: { id: user.id }, status: 'ACTIVE' },
      });
      if (notification) {
        await this.notificationsRepo.save({
          notification_token: notification,
          title,
          body,
          status: 'ACTIVE',
          created_by: user.username,
        });
        await firebase
          .messaging()
          .send({
            notification: { title, body },
            token: notification.notification_token,
            android: { priority: 'high' },
          })
          .catch((error: any) => {
            console.error(error);
          });
      }
    } catch (error) {
      return error;
    }
  };

sendPush finds an active notification token for the user using the notificationTokenRepo.findOne method; it searches for a token where the user’s ID and status are ACTIVE.

If a notification token is found, it saves the notification to the database using the notificationsRepo.save method, with the title, body, and status created by the user.

It sends the notification to the user’s device using the FCM firebase.messaging().send() method, passing in the title, body of the notification, the user’s notification token, and the priority set to high for Android.

  • token: The FCM registration token of the device to which the notification should be sent
  • title: The title of the notification
  • body: The body of the notification
  • android: Sets the device type priority

It’s worth noting that the sendPush method uses the firebase-admin package to send the push notifications, an official Firebase Admin SDK for Node.js that allows you to interact with Firebase services from a server environment.

Utilizing push notifications with users

Now that we’ve set up FCM and MySQL, we can use them to send push notifications to the users based on data in the MySQL database. Using the Firebase service, we can update the user service to enable, retrieve notifications from the database, and send them.

To demonstrate this, let’s use an update endpoint. As long as the user enables notifications, each time they update their profile, we call the sendPush method in the notification service, sending push notifications to the user and saving the notifications in the database.

Configuring user services

First, inject the NotificationService into the user service:

import { Injectable } from '@nestjs/common';
import { NotificationService } from 'src/notification/notification.service';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    private readonly notificationService: NotificationService,
  ) {}

  create(user: CreateUserDto): Promise<User> {}

  updateProfile = async (user_id: number, update_dto: any): Promise<any> => {}

  enablePush = async (
   user_id: number,
    update_dto: NotificationDto,
  ): Promise<any> => {};

  disablePush = async (
    user_id: number,
    update_dto: UpdateNotificationDto,
  ): Promise<any> => {};

  getPushNotifications = async (): Promise<any> => {};

}

The UsersService class has five methods:

create

create(user: CreateUserDto): Promise<User> {
    return this.userRepository.save(user);
  }

create creates a new user account and saves it to the database.

updateProfile

updateProfile = async (user_id: number, update_dto: any): Promise<any> => {
    try {
      const user = await this.userRepository.findOne({
        where: { id: user_id },
      });
      const updated_user = {
        ...user,
        username: update_dto.username,
        email: update_dto.email,
      }
      const saved_user = await this.userRepository.save(updated_user);
      if (saved_user) {
        // send push notification
        await this.notificationService.sendPush(
          updated_user,
          'Profiie update',
          'Your Profile have been updated successfully',
        )
        .catch((e) => {
          console.log('Error sending push notification', e);
        }); 
      }
      return saved_user;
    } catch (error) {
      return error;
    }
  }

updateProfile creates a new user object with the provided user object, updating the username and email from the update DTO.

It saves the updated user object to the database using the userRepository.save method. If the user is saved successfully, it sends a push notification to the user using the notificationService.sendPush method, passing in the updated user object, the title Profile update, and the body reading Your Profile have been updated successfully.

It has a try...catch block to handle any errors that may occur while executing the method, returning the error if there are any.

enablePush

enablePush = async (
   user_id: number,
    update_dto: NotificationDto,
  ): Promise<any> => {
    const user = await this.userRepository.findOne({
      where: { id: user_id },
    });
    return await this.notificationService.acceptPushNotification(
      user,
      update_dto,
    );
  };

By searching for a user where the ID matches the provided user ID, enablePush finds the user in the database using the userRepository.findOne method.

Then, it calls the notificationService.acceptPushNotification method and passes the user and update DTO as arguments. This grants users the permission to accept any push notification sent, or in our case, a push notification when a user profile is updated.

disablePush

disablePush = async (
    user_id: number,
    update_dto: UpdateNotificationDto,
  ): Promise<any> => {
    const user = await this.userRepository.findOne({
      where: { id: user_id },
    });
    return await this.notificationService.disablePushNotification(
      user,
      update_dto,
    );
  };

By searching for a user where the ID matches the provided user ID, disablePush finds the user in the database using the userRepository.findOne method. It then calls the notificationService.disablePushNotification method, passing the user and update DTO as arguments.

getPushNotifications

getPushNotifications = async (): Promise<any> => {
    return await this.notificationService.getNotifications();
  };

getPushNotifications calls the notificationService.getNotifications() method and returns the result.

It’s worth noting that these methods use the userRepository and notificationService to interact with the database and handle push notifications, respectively. It could be considered a higher-level service that uses the specific services to handle the push notifications and user profiles separately.

Configuring user controllers

Now that our user services are configured with the five methods mentioned above, we’ll create the different endpoints as controllers by initializing the private instance of the UsersService class, which is responsible for performing the actual user operations:

import { Controller, Get, Post, Body, Patch, Param, Delete, HttpStatus, HttpCode, Put } from '@nestjs/common';
import { NotificationDto } from 'src/notification/dto/create-notification.dto';
import { UpdateNotificationDto } from 'src/notification/dto/update-notification.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @Post()
  @HttpCode(HttpStatus.OK)
  async CreateUser(@Body() user: CreateUserDto) {
    return await this.usersService.create(user);
  }

  @Put()
  @HttpCode(HttpStatus.OK)
  async updateProfile(
    @Body() update_dto: any,
    @Param('id') user_id: number,
  ) {
    return await this.usersService.updateProfile(user_id ,update_dto);
  }

  @Put('push/enable')
  @HttpCode(HttpStatus.OK)
  async enablePush(
    @Body() update_dto: NotificationDto,
    @Param('id') user_id: number,
  ) {
    return await this.usersService.enablePush(user_id, update_dto)  
  }

  @Put('push/disable')
  @HttpCode(HttpStatus.OK)
  async disablePush(
    @Param('id') user_id: number,
    @Body() update_dto: UpdateNotificationDto,
  ) {
    return await this.usersService.disablePush(user_id, update_dto)
  }

  @Get('push/notifications')
  @HttpCode(HttpStatus.OK)
  async fetchPusNotifications() {
    return await this.usersService.getPushNotifications();
  }
}

The @Controller('users') defines the base endpoint. The @Post decorator handles HTTP POST requests, which are used to create a new user. To create a new user, the create method of the UsersService is called with the user information passed as @Body.

The @Put decorator handles HTTP PUT requests, which update a user’s profile. The updateProfile method of the UsersService is then called with the user ID as the request @Param and the updated user information to update the user’s profile.

Similarly, the @Put('push/enable') and @Put('push/disable') methods handle enabling or disabling push notifications for a user. The @Param decorator is used to extract the user ID from the URL, while the @Body decorator is extracts the information needed for enabling or disabling push notifications.

The enablePush and disablePush methods of the UsersService are then called with the user ID and the necessary information to enable or disable push notifications for the user. Finally, the @Get('push/notifications') method handles fetching push notifications. The getPushNotifications method of the UsersService is then called to fetch the push notifications.

The @HttpCode decorator is used to set the HTTP response code for each method, with the default being 200 (OK) for all methods.

Testing controller endpoints

To test our user controllers, we’ll use VS Code Thunder Client. First, let’s create a user:

Test Controllers VSCode Thunder Client

The user must enable push notifications in order to receive them. So, we’ll send our device_type and unique FCM token. Keep in mind that the FCM identification of your client app on a certain device is represented by the notification token.

When it first registers with FCM, your app obtains a registration token while operating on a device or the browser; the registration token is created at runtime.

notification_token:

Registration Token Created Runtime

The user has accepted the push notification and its notification token generated from the frontend being stored. Therefore, the user can update their profile and notification prefrences, save, and get the notification about their profile update when handled on the frontend:

User Accepted Push Notification

Finally, you can view all notifications:

View All Notifications Firebase NestJS

On the client side, you’ll need to use the Firebase JavaScript SDK to request permission to show push notifications and handle incoming push notifications.

Conclusion

In this article, we learned how to implement in-app notifications in a NestJS app using Firebase and MySQL. First, we set up the Firebase Admin SDK to send push notifications. We then set up a MySQL database to store notification data, and we created a service to retrieve and send notifications based on data in the database.

I hope you enjoyed this article. Leave a comment if you have any questions, and happy coding!

The post Implement in-app notifications with NestJS, MySQL, and Firebase appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/v0s8Jqa
Gain $200 in a week
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top