diff --git a/drips/notifications/controllers/notification.controller.ts b/drips/notifications/controllers/notification.controller.ts new file mode 100644 index 0000000..433f634 --- /dev/null +++ b/drips/notifications/controllers/notification.controller.ts @@ -0,0 +1,120 @@ +import { + Controller, + Get, + Patch, + Param, + Query, +// UseGuards, + Request, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { NotificationService } from '../services/notification.service'; +import { + ListNotificationsQueryDto, + PaginatedNotificationsResponseDto, + NotificationResponseDto, +} from '../dto/notification.dto'; +// import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; + +@ApiTags('Notifications') +@Controller('notifications') +// @UseGuards(JwtAuthGuard) +@ApiBearerAuth() +export class NotificationController { + constructor(private readonly notificationService: NotificationService) {} + + @Get() + @ApiOperation({ + summary: 'List user notifications', + description: + 'Retrieve paginated list of notifications for the authenticated user with optional filtering', + }) + @ApiResponse({ + status: 200, + description: 'Notifications retrieved successfully', + type: PaginatedNotificationsResponseDto, + }) + @ApiQuery({ name: 'read', required: false, type: Boolean }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ + name: 'type', + required: false, + enum: ['booking_accepted', 'session_upcoming', 'payment_released'], + }) + async list( + @Request() req, + @Query() query: ListNotificationsQueryDto, + ): Promise { + return this.notificationService.list(req.user.id, query); + } + + @Get('unread-count') + @ApiOperation({ + summary: 'Get unread notification count', + description: + 'Get the count of unread notifications for the authenticated user', + }) + @ApiResponse({ + status: 200, + description: 'Unread count retrieved successfully', + schema: { + type: 'object', + properties: { + count: { type: 'number', example: 5 }, + }, + }, + }) + async getUnreadCount(@Request() req): Promise<{ count: number }> { + const count = await this.notificationService.getUnreadCount(req.user.id); + return { count }; + } + + @Patch(':id/read') + @ApiOperation({ + summary: 'Mark notification as read', + description: + 'Mark a single notification as read. Only the owner can mark their notifications.', + }) + @ApiParam({ name: 'id', description: 'Notification ID' }) + @ApiResponse({ + status: 200, + description: 'Notification marked as read', + type: NotificationResponseDto, + }) + @ApiResponse({ status: 403, description: 'Forbidden - not the owner' }) + @ApiResponse({ status: 404, description: 'Notification not found' }) + async markAsRead( + @Request() req, + @Param('id') id: string, + ): Promise { + return this.notificationService.markAsRead(id, req.user.id); + } + + @Patch('read-all') + @ApiOperation({ + summary: 'Mark all notifications as read', + description: + 'Mark all unread notifications for the authenticated user as read', + }) + @ApiResponse({ + status: 200, + description: 'All notifications marked as read', + schema: { + type: 'object', + properties: { + updated: { type: 'number', example: 12 }, + }, + }, + }) + async markAllAsRead(@Request() req): Promise<{ updated: number }> { + return this.notificationService.markAllAsRead(req.user.id); + } +} diff --git a/drips/notifications/dto/notification.dto.ts b/drips/notifications/dto/notification.dto.ts new file mode 100644 index 0000000..82b8d04 --- /dev/null +++ b/drips/notifications/dto/notification.dto.ts @@ -0,0 +1,103 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsEnum, + IsOptional, + IsBoolean, + IsInt, + Min, + Max, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { NotificationType } from '../entities/notification.entity'; + +export class NotificationResponseDto { + @ApiProperty({ example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }) + id: string; + + @ApiProperty({ example: 'f47ac10b-58cc-4372-a567-0e02b2c3d480' }) + userId: string; + + @ApiProperty({ + enum: NotificationType, + example: NotificationType.BOOKING_ACCEPTED, + }) + type: NotificationType; + + @ApiProperty({ + type: 'object', + example: { + bookingId: 'booking-123', + mentorName: 'Sarah Johnson', + sessionDate: '2026-02-01T14:00:00Z', + }, + }) + payload: Record; + + @ApiProperty({ example: false }) + isRead: boolean; + + @ApiProperty({ example: '2026-01-26T10:30:00Z' }) + createdAt: Date; + + @ApiProperty({ example: '2026-01-26T10:30:00Z' }) + updatedAt: Date; +} + +export class ListNotificationsQueryDto { + @ApiPropertyOptional({ description: 'Filter by read status' }) + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + read?: boolean; + + @ApiPropertyOptional({ description: 'Page number', minimum: 1, default: 1 }) + @IsOptional() + @IsInt() + @Min(1) + @Type(() => Number) + page?: number = 1; + + @ApiPropertyOptional({ + description: 'Items per page', + minimum: 1, + maximum: 100, + default: 10, + }) + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + @Type(() => Number) + limit?: number = 10; + + @ApiPropertyOptional({ + enum: NotificationType, + description: 'Filter by notification type', + }) + @IsOptional() + @IsEnum(NotificationType) + type?: NotificationType; +} + +export class PaginatedNotificationsResponseDto { + @ApiProperty({ type: [NotificationResponseDto] }) + items: NotificationResponseDto[]; + + @ApiProperty({ example: 1 }) + page: number; + + @ApiProperty({ example: 10 }) + limit: number; + + @ApiProperty({ example: 42 }) + total: number; + + @ApiProperty({ example: 5 }) + totalPages: number; +} + +export class CreateNotificationDto { + userId: string; + type: NotificationType; + payload: Record; +} diff --git a/drips/notifications/entities/notification.entity.ts b/drips/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..9f6752e --- /dev/null +++ b/drips/notifications/entities/notification.entity.ts @@ -0,0 +1,50 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { User } from 'user/user.entity'; + +export enum NotificationType { + BOOKING_ACCEPTED = 'booking_accepted', + SESSION_UPCOMING = 'session_upcoming', + PAYMENT_RELEASED = 'payment_released', +} + +@Entity('notifications') +@Index(['userId', 'createdAt']) +@Index(['userId', 'isRead']) +export class Notification { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('uuid') + @Index() + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + user: User; + + @Column({ + type: 'enum', + enum: NotificationType, + }) + @Index() + type: NotificationType; + + @Column('jsonb') + payload: Record; + + @Column({ default: false }) + isRead: boolean; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/drips/notifications/notifications.module.ts b/drips/notifications/notifications.module.ts index d2df60e..b642bc5 100644 --- a/drips/notifications/notifications.module.ts +++ b/drips/notifications/notifications.module.ts @@ -2,10 +2,15 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { EmailService } from './services/email.service'; import { TemplateService } from './services/template.service'; +import { Notification } from './entities/notification.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationController } from './controllers/notification.controller'; +import { NotificationService } from './services/notification.service'; @Module({ - imports: [ConfigModule], - providers: [EmailService, TemplateService], - exports: [EmailService], + imports: [TypeOrmModule.forFeature([Notification]), ConfigModule], + controllers: [NotificationController], + providers: [EmailService, TemplateService, NotificationService], + exports: [EmailService, NotificationService], }) export class NotificationsModule {} diff --git a/drips/notifications/services/notification.service.ts b/drips/notifications/services/notification.service.ts new file mode 100644 index 0000000..c87aaaf --- /dev/null +++ b/drips/notifications/services/notification.service.ts @@ -0,0 +1,187 @@ +import { + Injectable, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { + Notification, + NotificationType, +} from '../entities/notification.entity'; +import { + CreateNotificationDto, + ListNotificationsQueryDto, + PaginatedNotificationsResponseDto, + NotificationResponseDto, +} from '../dto/notification.dto'; + +@Injectable() +export class NotificationService { + constructor( + @InjectRepository(Notification) + private readonly notificationRepository: Repository, + ) {} + + /** + * Create a new notification (called by other services/modules) + */ + async create(dto: CreateNotificationDto): Promise { + const notification = this.notificationRepository.create({ + userId: dto.userId, + type: dto.type, + payload: dto.payload, + isRead: false, + }); + + return this.notificationRepository.save(notification); + } + + /** + * Bulk create notifications for multiple users + */ + async createBulk( + notifications: CreateNotificationDto[], + ): Promise { + const entities = notifications.map((dto) => + this.notificationRepository.create({ + userId: dto.userId, + type: dto.type, + payload: dto.payload, + isRead: false, + }), + ); + + return this.notificationRepository.save(entities); + } + + /** + * List notifications for a user with filtering and pagination + */ + async list( + userId: string, + query: ListNotificationsQueryDto, + ): Promise { + const { page = 1, limit = 10, read, type } = query; + const skip = (page - 1) * limit; + + const queryBuilder = this.notificationRepository + .createQueryBuilder('notification') + .where('notification.userId = :userId', { userId }) + .orderBy('notification.createdAt', 'DESC'); + + // Apply filters + if (read !== undefined) { + queryBuilder.andWhere('notification.isRead = :read', { read }); + } + + if (type) { + queryBuilder.andWhere('notification.type = :type', { type }); + } + + // Get total count + const total = await queryBuilder.getCount(); + + // Get paginated items + const items = await queryBuilder.skip(skip).take(limit).getMany(); + + return { + items: items.map(this.toResponseDto), + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }; + } + + /** + * Get a single notification (with ownership check) + */ + async findOne(id: string, userId: string): Promise { + const notification = await this.notificationRepository.findOne({ + where: { id }, + }); + + if (!notification) { + throw new NotFoundException('Notification not found'); + } + + if (notification.userId !== userId) { + throw new ForbiddenException( + 'You do not have access to this notification', + ); + } + + return notification; + } + + /** + * Mark a single notification as read + */ + async markAsRead( + id: string, + userId: string, + ): Promise { + const notification = await this.findOne(id, userId); + + if (!notification.isRead) { + notification.isRead = true; + await this.notificationRepository.save(notification); + } + + return this.toResponseDto(notification); + } + + /** + * Mark all user notifications as read + */ + async markAllAsRead(userId: string): Promise<{ updated: number }> { + const result = await this.notificationRepository + .createQueryBuilder() + .update(Notification) + .set({ isRead: true, updatedAt: new Date() }) + .where('userId = :userId', { userId }) + .andWhere('isRead = :isRead', { isRead: false }) + .execute(); + + return { updated: result.affected || 0 }; + } + + /** + * Get unread count for a user + */ + async getUnreadCount(userId: string): Promise { + return this.notificationRepository.count({ + where: { userId, isRead: false }, + }); + } + + /** + * Delete old read notifications (cleanup job) + */ + async deleteOldReadNotifications(daysOld: number = 90): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysOld); + + const result = await this.notificationRepository + .createQueryBuilder() + .delete() + .where('isRead = :isRead', { isRead: true }) + .andWhere('createdAt < :cutoffDate', { cutoffDate }) + .execute(); + + return result.affected || 0; + } + + private toResponseDto(notification: Notification): NotificationResponseDto { + return { + id: notification.id, + userId: notification.userId, + type: notification.type, + payload: notification.payload, + isRead: notification.isRead, + createdAt: notification.createdAt, + updatedAt: notification.updatedAt, + }; + } +} diff --git a/drips/package-lock.json b/drips/package-lock.json index eed1b4a..c75c18b 100644 --- a/drips/package-lock.json +++ b/drips/package-lock.json @@ -235,7 +235,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2230,7 +2229,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.12.tgz", "integrity": "sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -2272,19 +2270,12 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/config/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/@nestjs/core": { "version": "11.1.12", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.12.tgz", "integrity": "sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2320,6 +2311,19 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", @@ -2338,17 +2342,6 @@ "class-validator": { "optional": true } - "node_modules/@nestjs/jwt": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", - "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "9.0.10", - "jsonwebtoken": "9.0.3" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, "node_modules/@nestjs/passport": { @@ -2366,7 +2359,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz", "integrity": "sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.2.1", @@ -2514,12 +2506,6 @@ } } }, - "node_modules/@nestjs/swagger/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/@nestjs/testing": { "version": "11.1.12", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.12.tgz", @@ -2623,12 +2609,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -2643,7 +2623,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -2695,6 +2674,13 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -2875,7 +2861,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3013,7 +2998,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3152,7 +3136,6 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -3834,7 +3817,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3884,7 +3866,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4233,9 +4214,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", - "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4322,7 +4303,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4422,7 +4402,6 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz", "integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==", "license": "MIT", - "peer": true, "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" @@ -4568,7 +4547,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -4616,15 +4594,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -5176,9 +5152,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.277", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", - "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", + "version": "1.5.278", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", + "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", "dev": true, "license": "ISC" }, @@ -5321,7 +5297,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5382,7 +5357,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6471,7 +6445,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", @@ -6741,7 +6714,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7638,7 +7610,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -7730,10 +7701,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, "node_modules/lodash.defaults": { @@ -8422,7 +8392,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -8533,9 +8502,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8572,7 +8541,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", @@ -8830,7 +8798,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9048,8 +9015,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/require-directory": { "version": "2.1.1", @@ -9145,7 +9111,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -9828,7 +9793,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10170,7 +10134,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10331,7 +10294,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -10537,7 +10499,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10823,7 +10784,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -10893,7 +10853,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1",