mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +00:00
feat: working on push notifications journey
This commit is contained in:
1185
package-lock.json
generated
1185
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -49,6 +49,7 @@
|
||||
"cacheable": "^1.8.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"firebase-admin": "^13.0.2",
|
||||
"google-libphonenumber": "^3.2.39",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.4.1",
|
||||
|
@ -10,6 +10,7 @@ import { AllowanceModule } from './allowance/allowance.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { CacheModule } from './common/modules/cache/cache.module';
|
||||
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||
import { NotificationModule } from './common/modules/notification/notification.module';
|
||||
import { OtpModule } from './common/modules/otp/otp.module';
|
||||
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||
@ -70,6 +71,8 @@ import { TaskModule } from './task/task.module';
|
||||
LookupModule,
|
||||
|
||||
HealthModule,
|
||||
|
||||
NotificationModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Pipes
|
||||
|
@ -19,6 +19,6 @@ import { AccessTokenStrategy } from './strategies';
|
||||
],
|
||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [UserService],
|
||||
exports: [UserService, DeviceService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsEnum, IsString, ValidateIf } from 'class-validator';
|
||||
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { GrantType } from '~/auth/enums';
|
||||
export class LoginRequestDto {
|
||||
@ -17,8 +17,14 @@ export class LoginRequestDto {
|
||||
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ example: 'device-token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceToken' }) })
|
||||
@ApiProperty({ example: 'fcm-device-token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.fcmToken' }) })
|
||||
@IsOptional()
|
||||
fcmToken?: string;
|
||||
|
||||
@ApiProperty({ example: 'Login signature' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.signature' }) })
|
||||
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
|
||||
deviceToken!: string;
|
||||
signature!: string;
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ export class Device {
|
||||
@Column('varchar', { name: 'public_key', nullable: true })
|
||||
publicKey?: string | null;
|
||||
|
||||
@Column('varchar', { name: 'fcm_token', nullable: true })
|
||||
fcmToken?: string | null;
|
||||
|
||||
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
||||
lastAccessOn!: Date;
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Notification } from '~/common/modules/notification/entities';
|
||||
import { Otp } from '~/common/modules/otp/entities';
|
||||
import { Customer } from '~/customer/entities/customer.entity';
|
||||
import { Roles } from '../enums';
|
||||
@ -54,6 +55,9 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => Device, (device) => device.user)
|
||||
devices!: Device[];
|
||||
|
||||
@OneToMany(() => Notification, (notification) => notification.user)
|
||||
notifications!: Notification[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Device } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
@ -16,6 +16,10 @@ export class DeviceRepository {
|
||||
}
|
||||
|
||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||
return this.deviceRepository.update({ deviceId }, data);
|
||||
return this.deviceRepository.save({ deviceId, ...data });
|
||||
}
|
||||
|
||||
getTokens(userId: string) {
|
||||
return this.deviceRepository.find({ where: { userId, fcmToken: Not(IsNull()) }, select: ['fcmToken'] });
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +187,11 @@ export class AuthService {
|
||||
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
|
||||
}
|
||||
|
||||
this.deviceService.updateDevice(deviceId, { lastAccessOn: new Date() });
|
||||
this.deviceService.updateDevice(deviceId, {
|
||||
lastAccessOn: new Date(),
|
||||
fcmToken: loginDto.fcmToken,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return [tokens, user];
|
||||
}
|
||||
@ -245,7 +249,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
|
||||
}
|
||||
|
||||
const cleanToken = removePadding(loginDto.deviceToken);
|
||||
const cleanToken = removePadding(loginDto.signature);
|
||||
const isValidToken = await verifySignature(
|
||||
device.publicKey,
|
||||
cleanToken,
|
||||
|
@ -16,4 +16,10 @@ export class DeviceService {
|
||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||
return this.deviceRepository.updateDevice(deviceId, data);
|
||||
}
|
||||
|
||||
async getTokens(userId: string): Promise<string[]> {
|
||||
const devices = await this.deviceRepository.getTokens(userId);
|
||||
|
||||
return devices.map((device) => device.fcmToken!);
|
||||
}
|
||||
}
|
||||
|
1
src/common/modules/notification/controllers/index.ts
Normal file
1
src/common/modules/notification/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './notifications.controller';
|
@ -0,0 +1,36 @@
|
||||
import { Controller, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { NotificationsPageResponseDto } from '../dtos/response';
|
||||
import { NotificationsService } from '../services/notifications.service';
|
||||
|
||||
@Controller('notifications')
|
||||
@ApiTags('Notifications')
|
||||
@ApiBearerAuth()
|
||||
export class NotificationsController {
|
||||
constructor(private readonly notificationsService: NotificationsService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiResponse({ type: NotificationsPageResponseDto })
|
||||
async getNotifications(@AuthenticatedUser() { sub }: IJwtPayload, @Query() pageOptionsDto: PageOptionsRequestDto) {
|
||||
const { notifications, count, unreadCount } = await this.notificationsService.getNotifications(sub, pageOptionsDto);
|
||||
|
||||
return new NotificationsPageResponseDto(notifications, {
|
||||
itemCount: count,
|
||||
unreadCount,
|
||||
page: pageOptionsDto.page,
|
||||
size: pageOptionsDto.size,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('mark-as-read')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
markAsRead(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
return this.notificationsService.markAsRead(sub);
|
||||
}
|
||||
}
|
2
src/common/modules/notification/dtos/response/index.ts
Normal file
2
src/common/modules/notification/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './notifications-page.response.dto';
|
||||
export * from './notifications.response.dto';
|
@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PageMetaResponseDto } from '~/core/dtos';
|
||||
import { INotificationPageMeta } from '../../interfaces';
|
||||
|
||||
export class NotificationMetaResponseDto extends PageMetaResponseDto {
|
||||
@ApiProperty({ example: 4 })
|
||||
readonly unreadCount!: number;
|
||||
|
||||
constructor(meta: INotificationPageMeta) {
|
||||
super(meta);
|
||||
this.unreadCount = meta.unreadCount;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Notification } from '../../entities';
|
||||
import { INotificationPageMeta } from '../../interfaces';
|
||||
import { NotificationMetaResponseDto } from './notifications-meta.response.dto';
|
||||
import { NotificationsResponseDto } from './notifications.response.dto';
|
||||
|
||||
export class NotificationsPageResponseDto {
|
||||
@ApiProperty({ type: [NotificationsResponseDto] })
|
||||
data!: NotificationsResponseDto[];
|
||||
|
||||
@ApiProperty({ type: NotificationMetaResponseDto })
|
||||
meta: INotificationPageMeta;
|
||||
|
||||
constructor(data: Notification[], meta: INotificationPageMeta) {
|
||||
this.data = data.map((notification) => new NotificationsResponseDto(notification));
|
||||
this.meta = new NotificationMetaResponseDto(meta);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Notification } from '../../entities';
|
||||
import { NotificationStatus } from '../../enums';
|
||||
|
||||
export class NotificationsResponseDto {
|
||||
@ApiProperty({ example: 'f1b1b9b1-1b1b-1b1b-1b1b-1b1b1b1b1b1b' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'Test' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'TestBody' })
|
||||
body: string;
|
||||
|
||||
@ApiProperty({ example: NotificationStatus.UNREAD })
|
||||
status!: NotificationStatus;
|
||||
|
||||
@ApiProperty({ example: '2021-09-01T00:00:00.000Z' })
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(notification: Notification) {
|
||||
this.id = notification.id;
|
||||
this.title = notification.title;
|
||||
this.body = notification.message;
|
||||
this.status = notification.status!;
|
||||
this.createdAt = notification.createdAt;
|
||||
}
|
||||
}
|
1
src/common/modules/notification/entities/index.ts
Normal file
1
src/common/modules/notification/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './notification.entity';
|
@ -0,0 +1,48 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { NotificationChannel, NotificationScope, NotificationStatus } from '../enums';
|
||||
|
||||
@Entity('notifications')
|
||||
export class Notification {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('varchar', { name: 'title' })
|
||||
title!: string;
|
||||
|
||||
@Column('varchar', { name: 'message' })
|
||||
message!: string;
|
||||
|
||||
@Column('varchar', { name: 'recipient', nullable: true })
|
||||
recipient?: string | null;
|
||||
|
||||
@Column('varchar', { name: 'scope' })
|
||||
scope!: NotificationScope;
|
||||
|
||||
@Column('varchar', { name: 'status', nullable: true })
|
||||
status!: NotificationStatus | null;
|
||||
|
||||
@Column('varchar', { name: 'channel' })
|
||||
channel!: NotificationChannel;
|
||||
|
||||
@Column('uuid', { name: 'user_id', nullable: true })
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.notifications, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
3
src/common/modules/notification/enums/index.ts
Normal file
3
src/common/modules/notification/enums/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './notification-channel.enum';
|
||||
export * from './notification-scope.enum';
|
||||
export * from './notification-status.enum';
|
@ -0,0 +1,5 @@
|
||||
export enum NotificationChannel {
|
||||
EMAIL = 'EMAIL',
|
||||
SMS = 'SMS',
|
||||
PUSH = 'PUSH',
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export enum NotificationScope {
|
||||
USER_REGISTERED = 'USER_REGISTERED',
|
||||
TASK_COMPLETED = 'TASK_COMPLETED',
|
||||
GIFT_RECEIVED = 'GIFT_RECEIVED',
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export enum NotificationStatus {
|
||||
READ = 'READ',
|
||||
UNREAD = 'UNREAD',
|
||||
}
|
1
src/common/modules/notification/interfaces/index.ts
Normal file
1
src/common/modules/notification/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './notification-page-meta.interface';
|
@ -0,0 +1,5 @@
|
||||
import { IPageMeta } from '~/core/dtos';
|
||||
|
||||
export interface INotificationPageMeta extends IPageMeta {
|
||||
unreadCount: number;
|
||||
}
|
16
src/common/modules/notification/notification.module.ts
Normal file
16
src/common/modules/notification/notification.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
import { NotificationsController } from './controllers';
|
||||
import { Notification } from './entities';
|
||||
import { NotificationsRepository } from './repositories';
|
||||
import { FirebaseService } from './services/firebase.service';
|
||||
import { NotificationsService } from './services/notifications.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Notification]), AuthModule],
|
||||
providers: [NotificationsService, FirebaseService, NotificationsRepository],
|
||||
exports: [NotificationsService],
|
||||
controllers: [NotificationsController],
|
||||
})
|
||||
export class NotificationModule {}
|
1
src/common/modules/notification/repositories/index.ts
Normal file
1
src/common/modules/notification/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './notifications.repository';
|
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { Notification } from '../entities';
|
||||
import { NotificationChannel, NotificationStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class NotificationsRepository {
|
||||
constructor(@InjectRepository(Notification) private readonly notificationsRepository: Repository<Notification>) {}
|
||||
|
||||
getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
||||
const readQuery = this.notificationsRepository.createQueryBuilder('notification');
|
||||
readQuery.where('notification.userId = :userId', { userId });
|
||||
readQuery.andWhere('notification.channel = :channel', { channel: NotificationChannel.PUSH });
|
||||
readQuery.orderBy('notification.createdAt', 'DESC');
|
||||
readQuery.skip((pageOptionsDto.page - ONE) * pageOptionsDto.size);
|
||||
readQuery.take(pageOptionsDto.size);
|
||||
return readQuery.getManyAndCount();
|
||||
}
|
||||
|
||||
getUnreadNotificationsCount(userId: string) {
|
||||
return this.notificationsRepository.count({ where: { userId, status: NotificationStatus.UNREAD } });
|
||||
}
|
||||
|
||||
markAsRead(userId: string) {
|
||||
return this.notificationsRepository.update(
|
||||
{ userId, status: NotificationStatus.UNREAD },
|
||||
{ status: NotificationStatus.READ },
|
||||
);
|
||||
}
|
||||
|
||||
createNotification(notification: Partial<Notification>) {
|
||||
return this.notificationsRepository.save(notification);
|
||||
}
|
||||
}
|
27
src/common/modules/notification/services/firebase.service.ts
Normal file
27
src/common/modules/notification/services/firebase.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as admin from 'firebase-admin';
|
||||
@Injectable()
|
||||
export class FirebaseService {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert({
|
||||
projectId: this.configService.get('FIREBASE_PROJECT_ID'),
|
||||
clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'),
|
||||
privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
sendNotification(tokens: string | string[], title: string, body: string) {
|
||||
const message = {
|
||||
notification: {
|
||||
title,
|
||||
body,
|
||||
},
|
||||
tokens: Array.isArray(tokens) ? tokens : [tokens],
|
||||
};
|
||||
|
||||
admin.messaging().sendEachForMulticast(message);
|
||||
}
|
||||
}
|
0
src/common/modules/notification/services/index.ts
Normal file
0
src/common/modules/notification/services/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DeviceService } from '~/auth/services';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { Notification } from '../entities';
|
||||
import { NotificationsRepository } from '../repositories';
|
||||
import { FirebaseService } from './firebase.service';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
constructor(
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly firebaseService: FirebaseService,
|
||||
private readonly notificationRepository: NotificationsRepository,
|
||||
) {}
|
||||
|
||||
async sendPushNotification(userId: string, title: string, body: string) {
|
||||
// Get the device tokens for the user
|
||||
|
||||
const tokens = await this.deviceService.getTokens(userId);
|
||||
|
||||
if (!tokens.length) {
|
||||
return;
|
||||
}
|
||||
// Send the notification
|
||||
return this.firebaseService.sendNotification(tokens, title, body);
|
||||
}
|
||||
|
||||
async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
||||
const [[notifications, count], unreadCount] = await Promise.all([
|
||||
this.notificationRepository.getNotifications(userId, pageOptionsDto),
|
||||
this.notificationRepository.getUnreadNotificationsCount(userId),
|
||||
]);
|
||||
|
||||
return { notifications, count, unreadCount };
|
||||
}
|
||||
|
||||
createNotification(notification: Partial<Notification>) {
|
||||
return this.notificationRepository.createNotification(notification);
|
||||
}
|
||||
|
||||
markAsRead(userId: string) {
|
||||
return this.notificationRepository.markAsRead(userId);
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
const httpCtx: HttpArgumentsHost = host.switchToHttp();
|
||||
const res = httpCtx.getResponse<ExpressResponse>();
|
||||
const i18n = new I18nContextWrapper(I18nContext.current());
|
||||
|
||||
try {
|
||||
const status = this.extractStatusCode(exception);
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateNotificationEntityAndEditDevice1734944692999 implements MigrationInterface {
|
||||
name = 'CreateNotificationEntityAndEditDevice1734944692999';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notifications"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"title" character varying NOT NULL,
|
||||
"message" character varying NOT NULL,
|
||||
"recipient" character varying,
|
||||
"scope" character varying NOT NULL,
|
||||
"status" character varying,
|
||||
"channel" character varying NOT NULL,
|
||||
"user_id" uuid,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "devices" ADD "fcm_token" character varying`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" ADD CONSTRAINT "FK_9a8a82462cab47c73d25f49261f" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "notifications" DROP CONSTRAINT "FK_9a8a82462cab47c73d25f49261f"`);
|
||||
await queryRunner.query(`ALTER TABLE "devices" DROP COLUMN "fcm_token"`);
|
||||
await queryRunner.query(`DROP TABLE "notifications"`);
|
||||
}
|
||||
}
|
@ -16,3 +16,4 @@ export * from './1734262619426-create-junior-registration-token-table';
|
||||
export * from './1734503895302-create-money-request-entity';
|
||||
export * from './1734601976591-create-allowance-entities';
|
||||
export * from './1734861516657-create-gift-entities';
|
||||
export * from './1734944692999-create-notification-entity-and-edit-device';
|
||||
|
@ -4,7 +4,6 @@ import { Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CreateGoalRequestDto } from '../dtos/request';
|
||||
import { Category, SavingGoal } from '../entities';
|
||||
const ZERO = 0;
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class SavingGoalsRepository {
|
||||
|
@ -7,7 +7,6 @@ import { SavingGoal } from '../entities';
|
||||
import { IGoalStats } from '../interfaces';
|
||||
import { SavingGoalsRepository } from '../repositories';
|
||||
import { CategoryService } from './category.service';
|
||||
const ZERO = 0;
|
||||
@Injectable()
|
||||
export class SavingGoalsService {
|
||||
constructor(
|
||||
|
Reference in New Issue
Block a user