announcments
This commit is contained in:
@@ -4,6 +4,118 @@
|
|||||||
],
|
],
|
||||||
"name": "public",
|
"name": "public",
|
||||||
"tables": [
|
"tables": [
|
||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(26)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 26,
|
||||||
|
"mappedType": "character"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 6,
|
||||||
|
"default": "now()",
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 6,
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 100,
|
||||||
|
"mappedType": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"publish_at": {
|
||||||
|
"name": "publish_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 6,
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"is_important": {
|
||||||
|
"name": "is_important",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"name": "is_public",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "announcements",
|
||||||
|
"schema": "public",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"keyName": "announcements_is_public_created_at_index",
|
||||||
|
"columnNames": [
|
||||||
|
"is_public",
|
||||||
|
"created_at"
|
||||||
|
],
|
||||||
|
"composite": true,
|
||||||
|
"constraint": false,
|
||||||
|
"primary": false,
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyName": "announcements_pkey",
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"composite": false,
|
||||||
|
"constraint": true,
|
||||||
|
"primary": true,
|
||||||
|
"unique": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": [],
|
||||||
|
"foreignKeys": {},
|
||||||
|
"nativeEnums": {}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -728,7 +840,8 @@
|
|||||||
"create_print",
|
"create_print",
|
||||||
"view_print",
|
"view_print",
|
||||||
"manage_learnings",
|
"manage_learnings",
|
||||||
"manage_criticisms"
|
"manage_criticisms",
|
||||||
|
"manage_announcements"
|
||||||
],
|
],
|
||||||
"mappedType": "enum"
|
"mappedType": "enum"
|
||||||
},
|
},
|
||||||
@@ -3474,6 +3587,113 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nativeEnums": {}
|
"nativeEnums": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(26)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 26,
|
||||||
|
"mappedType": "character"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 6,
|
||||||
|
"default": "now()",
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 6,
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(26)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 26,
|
||||||
|
"mappedType": "character"
|
||||||
|
},
|
||||||
|
"announcement_id": {
|
||||||
|
"name": "announcement_id",
|
||||||
|
"type": "char(26)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 26,
|
||||||
|
"mappedType": "character"
|
||||||
|
},
|
||||||
|
"is_read": {
|
||||||
|
"name": "is_read",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "user_announcements",
|
||||||
|
"schema": "public",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"keyName": "user_announcements_pkey",
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"composite": false,
|
||||||
|
"constraint": true,
|
||||||
|
"primary": true,
|
||||||
|
"unique": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": [],
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_announcements_user_id_foreign": {
|
||||||
|
"constraintName": "user_announcements_user_id_foreign",
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"localTableName": "public.user_announcements",
|
||||||
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"referencedTableName": "public.users",
|
||||||
|
"updateRule": "cascade"
|
||||||
|
},
|
||||||
|
"user_announcements_announcement_id_foreign": {
|
||||||
|
"constraintName": "user_announcements_announcement_id_foreign",
|
||||||
|
"columnNames": [
|
||||||
|
"announcement_id"
|
||||||
|
],
|
||||||
|
"localTableName": "public.user_announcements",
|
||||||
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"referencedTableName": "public.announcements",
|
||||||
|
"updateRule": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nativeEnums": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nativeEnums": {}
|
"nativeEnums": {}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20260222092921 extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`create table "announcements" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "title" varchar(100) not null, "content" text not null, "publish_at" timestamptz null, "is_important" boolean not null default false, "is_public" boolean not null default false, constraint "announcements_pkey" primary key ("id"));`);
|
||||||
|
this.addSql(`create index "announcements_is_public_created_at_index" on "announcements" ("is_public", "created_at");`);
|
||||||
|
|
||||||
|
this.addSql(`create table "user_announcements" ("id" char(26) not null, "created_at" timestamptz not null default now(), "deleted_at" timestamptz null, "user_id" char(26) not null, "announcement_id" char(26) not null, "is_read" boolean not null default false, constraint "user_announcements_pkey" primary key ("id"));`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "user_announcements" add constraint "user_announcements_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete cascade;`);
|
||||||
|
this.addSql(`alter table "user_announcements" add constraint "user_announcements_announcement_id_foreign" foreign key ("announcement_id") references "announcements" ("id") on update cascade on delete cascade;`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'create_print', 'view_print', 'manage_learnings', 'manage_criticisms', 'manage_announcements'));`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`alter table "permissions" drop constraint if exists "permissions_name_check";`);
|
||||||
|
this.addSql(`alter table "permissions" add constraint "permissions_name_check" check("name" in ('manage_products', 'manage_categories', 'view_requests', 'delete_request', 'update_request', 'view_invoices', 'create_invoice', 'update_invoice', 'delete_invoice', 'view_all_invoiced_orders', 'view_my_assigned_invoiced_requests', 'view_orders', 'view_assigned_orders', 'create_order', 'update_order', 'assign_designer', 'delete_order', 'manage_admins', 'manage_roles', 'manage_users', 'manage_payments', 'manage_settings', 'manage_form_builder', 'create_print', 'view_print', 'manage_learnings', 'manage_criticisms'));`);
|
||||||
|
|
||||||
|
this.addSql(`drop table if exists "user_announcements";`);
|
||||||
|
this.addSql(`drop table if exists "announcements";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { InvoiceModule } from './modules/invoice/invoice.module';
|
|||||||
import { OrderModule } from './modules/order/order.module';
|
import { OrderModule } from './modules/order/order.module';
|
||||||
import { LearningModule } from './modules/learnings/learning.module';
|
import { LearningModule } from './modules/learnings/learning.module';
|
||||||
import { CriticismModule } from './modules/criticisms/criticisms.module';
|
import { CriticismModule } from './modules/criticisms/criticisms.module';
|
||||||
|
import { AnnouncementModule } from './modules/announcements/announcement.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -52,6 +53,7 @@ import { CriticismModule } from './modules/criticisms/criticisms.module';
|
|||||||
OrderModule,
|
OrderModule,
|
||||||
LearningModule,
|
LearningModule,
|
||||||
CriticismModule,
|
CriticismModule,
|
||||||
|
AnnouncementModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -137,6 +137,26 @@ export const enum LearningMessage {
|
|||||||
VIDEO_DURATION_INVALID = 'مدت زمان ویدیو نامعتبر است',
|
VIDEO_DURATION_INVALID = 'مدت زمان ویدیو نامعتبر است',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum AnnouncementMessage {
|
||||||
|
NOT_FOUND = 'اطلاعیه مورد نظر یافت نشد',
|
||||||
|
TITLE_IS_REQUIRED = 'عنوان الزامی است',
|
||||||
|
TITLE_STRING = 'عنوان باید رشته باشد',
|
||||||
|
TITLE_LENGTH = 'عنوان باید بین ۵ تا ۱۰۰ کاراکتر باشد',
|
||||||
|
CONTENT_IS_REQUIRED = 'محتوا الزامی است',
|
||||||
|
CONTENT_IS_STRING = 'محتوا باید رشته باشد',
|
||||||
|
CONTENT_MUST_BE_LONGER = 'محتوا باید حداقل ۱۰ کاراکتر باشد',
|
||||||
|
PUBLISH_AT_IS_REQUIRED = 'تاریخ انتشار الزامی است',
|
||||||
|
PUBLISH_AT_MUST_BE_DATE = 'تاریخ انتشار باید معتبر باشد',
|
||||||
|
PUBLISH_AT_MUST_BE_FUTURE_DATE = 'تاریخ انتشار باید در آینده باشد',
|
||||||
|
IMPORTANT_IS_REQUIRED = 'وضعیت اهمیت الزامی است',
|
||||||
|
IMPORTANT_MUST_BE_BOOLEAN = 'وضعیت اهمیت باید true یا false باشد',
|
||||||
|
USER_IDS_MUST_BE_ARRAY = 'لیست کاربران باید آرایه باشد',
|
||||||
|
USER_MUST_BE_UUID = 'شناسه کاربر باید معتبر باشد',
|
||||||
|
CREATED_AND_SENT_TO_USERS_AFTER_PUBLISH = 'اطلاعیه ایجاد شد و پس از انتشار به کاربران ارسال خواهد شد',
|
||||||
|
UPDATED_SUCCESSFULLY = 'اطلاعیه با موفقیت بهروزرسانی شد',
|
||||||
|
DELETED_SUCCESSFULLY = 'اطلاعیه با موفقیت حذف شد',
|
||||||
|
}
|
||||||
|
|
||||||
export const enum CriticismMessage {
|
export const enum CriticismMessage {
|
||||||
NOT_FOUND = 'انتقاد مورد نظر یافت نشد',
|
NOT_FOUND = 'انتقاد مورد نظر یافت نشد',
|
||||||
TITLE_IS_REQUIRED = 'عنوان الزامی است',
|
TITLE_IS_REQUIRED = 'عنوان الزامی است',
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export enum PermissionEnum {
|
|||||||
MANAGE_LEARNINGS = 'manage_learnings',
|
MANAGE_LEARNINGS = 'manage_learnings',
|
||||||
|
|
||||||
MANAGE_CRITICISMS = 'manage_criticisms',
|
MANAGE_CRITICISMS = 'manage_criticisms',
|
||||||
|
|
||||||
|
MANAGE_ANNOUNCEMENTS = 'manage_announcements',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,5 +82,6 @@ export const PermissionTitles: Record<PermissionEnum, string> = {
|
|||||||
[PermissionEnum.CREATE_PRINT]: "ایجاد فرم چاپ برای سفارش",
|
[PermissionEnum.CREATE_PRINT]: "ایجاد فرم چاپ برای سفارش",
|
||||||
[PermissionEnum.MANAGE_PRINT]: "مدیریت فرم چاپ",
|
[PermissionEnum.MANAGE_PRINT]: "مدیریت فرم چاپ",
|
||||||
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها",
|
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها",
|
||||||
[PermissionEnum.MANAGE_CRITICISMS]: "مدیریت انتقادها و پیشنهادها"
|
[PermissionEnum.MANAGE_CRITICISMS]: "مدیریت انتقادها و پیشنهادها",
|
||||||
|
[PermissionEnum.MANAGE_ANNOUNCEMENTS]: "مدیریت اطلاعیهها"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { AnnouncementMessage } from '../../../common/enums/message.enum';
|
||||||
|
|
||||||
|
export class CreateAnnouncementDto {
|
||||||
|
@IsNotEmpty({ message: AnnouncementMessage.TITLE_IS_REQUIRED })
|
||||||
|
@IsString({ message: AnnouncementMessage.TITLE_STRING })
|
||||||
|
@Length(5, 100, { message: AnnouncementMessage.TITLE_LENGTH })
|
||||||
|
@ApiProperty({ description: 'Title of the announcement', example: 'اطلاعیه جدید' })
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: AnnouncementMessage.CONTENT_IS_REQUIRED })
|
||||||
|
@IsString({ message: AnnouncementMessage.CONTENT_IS_STRING })
|
||||||
|
@MinLength(10, { message: AnnouncementMessage.CONTENT_MUST_BE_LONGER })
|
||||||
|
@ApiProperty({ description: 'Content of the announcement', example: 'اطلاعیه جدید برای کاربران' })
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString({}, { message: AnnouncementMessage.PUBLISH_AT_MUST_BE_DATE })
|
||||||
|
@ApiProperty({ description: 'Publish date of the announcement', example: '2025-02-25T10:00:00Z' })
|
||||||
|
publishAt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN })
|
||||||
|
@ApiProperty({ description: 'Is this announcement important?', example: false })
|
||||||
|
isImportant?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@IsArray({ message: AnnouncementMessage.USER_IDS_MUST_BE_ARRAY })
|
||||||
|
@IsString({ each: true, message: AnnouncementMessage.USER_MUST_BE_UUID })
|
||||||
|
@ApiProperty({
|
||||||
|
type: [String],
|
||||||
|
description: 'Array of user ids to send announcement to (if empty, sends to all users)',
|
||||||
|
example: [],
|
||||||
|
})
|
||||||
|
userIds?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsDateString, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { PaginationDto } from '../../../common/DTO/pagination.dto';
|
||||||
|
import { CriticismMessage } from '../../../common/enums/message.enum';
|
||||||
|
|
||||||
|
export class SearchAnnouncementQueryDto extends PaginationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
@ApiPropertyOptional({ description: 'since query', example: '2025-01-27' })
|
||||||
|
since?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
@ApiPropertyOptional({ description: 'publishAt query', example: '2025-01-27' })
|
||||||
|
publishAt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: CriticismMessage.SEARCH_QUERY_STRING })
|
||||||
|
@ApiPropertyOptional({ description: 'Search query', example: 'search query' })
|
||||||
|
q?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateAnnouncementDto } from './create-announcement.dto';
|
||||||
|
|
||||||
|
export class UpdateAnnouncementDto extends PartialType(CreateAnnouncementDto) {}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { CreateAnnouncementDto } from './DTO/create-announcement.dto';
|
||||||
|
import { SearchAnnouncementQueryDto } from './DTO/search-announcement-query.dto';
|
||||||
|
import { UpdateAnnouncementDto } from './DTO/update-announcement.dto';
|
||||||
|
import { AnnouncementService } from './providers/announcement.service';
|
||||||
|
import { AuthGuard } from '../auth/guards/auth.guard';
|
||||||
|
import { AdminAuthGuard } from '../auth/guards/adminAuth.guard';
|
||||||
|
import { Permissions } from '../../common/decorators/permissions.decorator';
|
||||||
|
import { UserId } from '../../common/decorators';
|
||||||
|
import { PermissionEnum } from '../../common/enums/permission.enum';
|
||||||
|
|
||||||
|
@ApiTags('Announcements')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller()
|
||||||
|
export class AnnouncementController {
|
||||||
|
constructor(private readonly announcementService: AnnouncementService) {}
|
||||||
|
|
||||||
|
|
||||||
|
@Get('/public/announcements/user')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get announcements for current user' })
|
||||||
|
getAnnouncementsByUser(@Query() queryDto: SearchAnnouncementQueryDto, @UserId() userId: string) {
|
||||||
|
return this.announcementService.getAnnouncementsByUser(queryDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/public/announcements/user/count')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get unread announcement count for user' })
|
||||||
|
countUserAnnouncements(@UserId() userId: string) {
|
||||||
|
return this.announcementService.countUserAnnouncements(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/public/announcements/:id')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get one announcement by id (user)' })
|
||||||
|
getAnnouncementById(@Param('id') id: string, @UserId() userId: string) {
|
||||||
|
return this.announcementService.getAnnouncementById(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------ Admin Routes -----------------------------
|
||||||
|
|
||||||
|
@Post('/admin/announcements')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_ANNOUNCEMENTS)
|
||||||
|
@ApiOperation({ summary: 'Create a new announcement (admin)' })
|
||||||
|
create(@Body() createAnnouncementDto: CreateAnnouncementDto) {
|
||||||
|
return this.announcementService.createAnnouncement(createAnnouncementDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('/admin/announcements/:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_ANNOUNCEMENTS)
|
||||||
|
@ApiOperation({ summary: 'Update an announcement (admin)' })
|
||||||
|
update(@Param('id') id: string, @Body() updateAnnouncementDto: UpdateAnnouncementDto) {
|
||||||
|
return this.announcementService.updateAnnouncement(id, updateAnnouncementDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/admin/announcements:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_ANNOUNCEMENTS)
|
||||||
|
@ApiOperation({ summary: 'Delete an announcement (admin)' })
|
||||||
|
delete(@Param('id') id: string) {
|
||||||
|
return this.announcementService.deleteAnnouncement(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/admin/announcements')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_ANNOUNCEMENTS)
|
||||||
|
@ApiOperation({ summary: 'Get all announcements (admin)' })
|
||||||
|
getAnnouncements(@Query() queryDto: SearchAnnouncementQueryDto) {
|
||||||
|
return this.announcementService.getAnnouncements(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Get('/admin/announcements/:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_ANNOUNCEMENTS)
|
||||||
|
@ApiOperation({ summary: 'Get one announcement by id (admin)' })
|
||||||
|
getAnnouncementByIdAdmin(@Param('id') id: string) {
|
||||||
|
return this.announcementService.getAnnouncementByIdAdmin(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { AnnouncementController } from './announcement.controller';
|
||||||
|
import { ANNOUNCEMENT } from './constants';
|
||||||
|
import { Announcement } from './entities/announcement.entity';
|
||||||
|
import { UserAnnouncement } from './entities/user-announcement.entity';
|
||||||
|
import { AnnouncementService } from './providers/announcement.service';
|
||||||
|
import { AnnouncementProcessor } from './queue/announcement.processor';
|
||||||
|
import { AnnouncementRepository } from './repositories/announcement.repository';
|
||||||
|
import { UserAnnouncementRepository } from './repositories/user-announcement.repository';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MikroOrmModule.forFeature([Announcement, UserAnnouncement]),
|
||||||
|
BullModule.registerQueue({ name: ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME }),
|
||||||
|
UserModule,
|
||||||
|
JwtModule,
|
||||||
|
],
|
||||||
|
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository, AnnouncementProcessor],
|
||||||
|
controllers: [AnnouncementController],
|
||||||
|
exports: [AnnouncementService],
|
||||||
|
})
|
||||||
|
export class AnnouncementModule {}
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
export const ANNOUNCEMENT = Object.freeze({
|
||||||
|
ANNOUNCEMENT_QUEUE_NAME: "announcement",
|
||||||
|
ANNOUNCEMENT_SEND_JOB_NAME: "sendAnnouncement",
|
||||||
|
ANNOUNCEMENT_PUBLISH_JOB_NAME: "publishAnnouncement",
|
||||||
|
//
|
||||||
|
ANNOUNCEMENT_SEND_JOB_PRIORITY: 1,
|
||||||
|
ANNOUNCEMENT_SEND_JOB_ATTEMPTS: 3,
|
||||||
|
ANNOUNCEMENT_SEND_JOB_BACKOFF: 5 * 1000,
|
||||||
|
ANNOUNCEMENT_SEND_JOB_TIMEOUT: 10000,
|
||||||
|
//
|
||||||
|
ANNOUNCEMENT_PUBLISH_JOB_PRIORITY: 1,
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Cascade, Collection, Entity, Index, OneToMany, OptionalProps, Property } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
|
import { UserAnnouncement } from './user-announcement.entity';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'announcements' })
|
||||||
|
@Index({ properties: ['isPublic', 'createdAt'] })
|
||||||
|
export class Announcement extends BaseEntity {
|
||||||
|
[OptionalProps]?: 'createdAt' | 'deletedAt';
|
||||||
|
|
||||||
|
@Property({ type: 'varchar', length: 100 })
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'text' })
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'timestamptz', nullable: true })
|
||||||
|
publishAt?: Date | null;
|
||||||
|
|
||||||
|
@Property({ default: false })
|
||||||
|
isImportant: boolean = false;
|
||||||
|
|
||||||
|
@Property({ default: false })
|
||||||
|
isPublic: boolean = false;
|
||||||
|
|
||||||
|
@OneToMany(() => UserAnnouncement, (ua) => ua.announcement, { cascade: [Cascade.ALL] })
|
||||||
|
userAnnouncements = new Collection<UserAnnouncement>(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { Announcement } from './announcement.entity';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'user_announcements' })
|
||||||
|
export class UserAnnouncement extends BaseEntity {
|
||||||
|
[OptionalProps]?: 'createdAt' | 'deletedAt';
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Announcement)
|
||||||
|
announcement!: Announcement;
|
||||||
|
|
||||||
|
@Property({ default: false })
|
||||||
|
isRead: boolean = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ISendAnnouncement {
|
||||||
|
announcementId: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
userIds?: string[];
|
||||||
|
}
|
||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { AnnouncementMessage } from '../../../common/enums/message.enum';
|
||||||
|
import { ANNOUNCEMENT } from '../constants';
|
||||||
|
import { CreateAnnouncementDto } from '../DTO/create-announcement.dto';
|
||||||
|
import { SearchAnnouncementQueryDto } from '../DTO/search-announcement-query.dto';
|
||||||
|
import { UpdateAnnouncementDto } from '../DTO/update-announcement.dto';
|
||||||
|
import { Announcement } from '../entities/announcement.entity';
|
||||||
|
import { UserAnnouncement } from '../entities/user-announcement.entity';
|
||||||
|
import { AnnouncementRepository } from '../repositories/announcement.repository';
|
||||||
|
import { UserAnnouncementRepository } from '../repositories/user-announcement.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnnouncementService {
|
||||||
|
constructor(
|
||||||
|
@InjectQueue(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME) private readonly announcementQueue: Queue,
|
||||||
|
private readonly announcementRepository: AnnouncementRepository,
|
||||||
|
private readonly userAnnouncementRepository: UserAnnouncementRepository,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto) {
|
||||||
|
const { userIds } = createAnnouncementDto;
|
||||||
|
|
||||||
|
let isPublic = true;
|
||||||
|
const publishAt = createAnnouncementDto.publishAt ? dayjs(createAnnouncementDto.publishAt).toDate() : dayjs().toDate();
|
||||||
|
|
||||||
|
if (dayjs(publishAt).isBefore(dayjs().subtract(1, 'day'))) {
|
||||||
|
throw new BadRequestException(AnnouncementMessage.PUBLISH_AT_MUST_BE_FUTURE_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIds && userIds.length > 0) {
|
||||||
|
isPublic = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcement = this.em.create(Announcement, {
|
||||||
|
title: createAnnouncementDto.title,
|
||||||
|
content: createAnnouncementDto.content,
|
||||||
|
publishAt,
|
||||||
|
isImportant: createAnnouncementDto.isImportant ?? false,
|
||||||
|
isPublic,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(announcement);
|
||||||
|
|
||||||
|
await this.announcementQueue.add(
|
||||||
|
ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME,
|
||||||
|
{
|
||||||
|
announcementId: announcement.id,
|
||||||
|
isPublic,
|
||||||
|
userIds: userIds ?? [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: dayjs(publishAt).diff(dayjs(), 'millisecond'),
|
||||||
|
attempts: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_ATTEMPTS,
|
||||||
|
backoff: { type: 'exponential', delay: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_BACKOFF },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: AnnouncementMessage.CREATED_AND_SENT_TO_USERS_AFTER_PUBLISH,
|
||||||
|
announcement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnouncements(queryDto: SearchAnnouncementQueryDto) {
|
||||||
|
const page = queryDto.page ?? 1;
|
||||||
|
const limit = queryDto.limit ?? 10;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (queryDto.since) {
|
||||||
|
where.createdAt = { $gte: new Date(queryDto.since) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.publishAt) {
|
||||||
|
where.publishAt = { $gte: new Date(queryDto.publishAt) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.q) {
|
||||||
|
where.title = { $like: `%${queryDto.q}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [announcements, count] = await this.em.findAndCount(Announcement, where, {
|
||||||
|
orderBy: { createdAt: 'DESC' },
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { announcements, count, paginate: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnouncementsByUser(queryDto: SearchAnnouncementQueryDto, userId: string) {
|
||||||
|
const page = queryDto.page ?? 1;
|
||||||
|
const limit = queryDto.limit ?? 10;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const announcementFilter: Record<string, unknown> = {};
|
||||||
|
if (queryDto.since) {
|
||||||
|
announcementFilter.createdAt = { $gte: new Date(queryDto.since) };
|
||||||
|
}
|
||||||
|
if (queryDto.publishAt) {
|
||||||
|
announcementFilter.publishAt = { $gte: new Date(queryDto.publishAt) };
|
||||||
|
}
|
||||||
|
if (queryDto.q) {
|
||||||
|
announcementFilter.title = { $like: `%${queryDto.q}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
user: { id: userId },
|
||||||
|
...(Object.keys(announcementFilter).length > 0 && { announcement: announcementFilter }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [userAnnouncements, count] = await this.em.findAndCount(UserAnnouncement, where, {
|
||||||
|
orderBy: { announcement: { createdAt: 'DESC' } },
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
populate: ['announcement'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { announcements: userAnnouncements, count, paginate: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnouncementById(id: string, userId: string) {
|
||||||
|
const announcement = await this.announcementRepository.findOne({ id });
|
||||||
|
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
const userAnnouncement = await this.userAnnouncementRepository.findOne({
|
||||||
|
announcement: { id },
|
||||||
|
user: { id: userId },
|
||||||
|
});
|
||||||
|
if (!userAnnouncement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
userAnnouncement.isRead = true;
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return { announcement };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnouncementByIdAdmin(id: string) {
|
||||||
|
const announcement = await this.announcementRepository.findOne({ id });
|
||||||
|
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
return {
|
||||||
|
announcement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async countUserAnnouncements(userId: string) {
|
||||||
|
return this.userAnnouncementRepository.count({
|
||||||
|
user: { id: userId },
|
||||||
|
isRead: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto) {
|
||||||
|
const announcement = await this.announcementRepository.findOne({ id });
|
||||||
|
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
const { userIds } = updateAnnouncementDto;
|
||||||
|
let isPublic = true;
|
||||||
|
|
||||||
|
if (userIds && userIds.length > 0) {
|
||||||
|
isPublic = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateAnnouncementDto.publishAt) {
|
||||||
|
if (dayjs(updateAnnouncementDto.publishAt).isBefore(dayjs().subtract(1, 'day'))) {
|
||||||
|
throw new BadRequestException(AnnouncementMessage.PUBLISH_AT_MUST_BE_FUTURE_DATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userIds: _userIds, ...updateFields } = updateAnnouncementDto;
|
||||||
|
this.em.assign(announcement, {
|
||||||
|
...updateFields,
|
||||||
|
publishAt: updateAnnouncementDto.publishAt ? dayjs(updateAnnouncementDto.publishAt).toDate() : undefined,
|
||||||
|
isImportant: updateAnnouncementDto.isImportant ?? announcement.isImportant,
|
||||||
|
});
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
if (updateAnnouncementDto.publishAt) {
|
||||||
|
const jobs = await this.announcementQueue.getJobs(['delayed', 'waiting', 'active']);
|
||||||
|
const existingJob = jobs.find((job) => job.data?.announcementId === id);
|
||||||
|
if (existingJob) await existingJob.remove();
|
||||||
|
|
||||||
|
await this.announcementQueue.add(
|
||||||
|
ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME,
|
||||||
|
{
|
||||||
|
announcementId: announcement.id,
|
||||||
|
isPublic,
|
||||||
|
userIds: userIds ?? [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: dayjs(announcement.publishAt ?? new Date()).diff(dayjs(), 'millisecond'),
|
||||||
|
attempts: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_ATTEMPTS,
|
||||||
|
backoff: { type: 'exponential', delay: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_BACKOFF },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: AnnouncementMessage.UPDATED_SUCCESSFULLY,
|
||||||
|
announcement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAnnouncement(id: string) {
|
||||||
|
const announcement = await this.announcementRepository.findOne({ id });
|
||||||
|
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
const jobs = await this.announcementQueue.getJobs(['delayed', 'waiting', 'active']);
|
||||||
|
const existingJob = jobs.find((job) => job.data?.announcementId === id);
|
||||||
|
if (existingJob) await existingJob.remove();
|
||||||
|
|
||||||
|
await this.em.nativeDelete(UserAnnouncement, { announcement: { id } });
|
||||||
|
await this.em.removeAndFlush(announcement);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: AnnouncementMessage.DELETED_SUCCESSFULLY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { Job } from 'bullmq';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { ANNOUNCEMENT } from '../constants';
|
||||||
|
import { Announcement } from '../entities/announcement.entity';
|
||||||
|
import { UserAnnouncement } from '../entities/user-announcement.entity';
|
||||||
|
import { ISendAnnouncement } from '../interfaces/ISendAnnouncement';
|
||||||
|
|
||||||
|
@Processor(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME)
|
||||||
|
export class AnnouncementProcessor extends WorkerHost {
|
||||||
|
protected readonly logger = new Logger(AnnouncementProcessor.name);
|
||||||
|
|
||||||
|
constructor(private readonly em: EntityManager) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(job: Job<ISendAnnouncement>): Promise<unknown> {
|
||||||
|
this.logger.log(`Processing job: ${job.name}, id: ${job.id}`);
|
||||||
|
|
||||||
|
switch (job.name) {
|
||||||
|
case ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME:
|
||||||
|
return await this.sendAnnouncement(job);
|
||||||
|
case ANNOUNCEMENT.ANNOUNCEMENT_PUBLISH_JOB_NAME:
|
||||||
|
return this.publishAnnouncement(job);
|
||||||
|
default:
|
||||||
|
this.logger.error(`Unknown job name: ${job.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendAnnouncement(job: Job<ISendAnnouncement>): Promise<void> {
|
||||||
|
this.logger.log(`Sending announcement: ${job.data.announcementId} to users.`);
|
||||||
|
|
||||||
|
const data = job.data;
|
||||||
|
|
||||||
|
await this.em.transactional(async (em) => {
|
||||||
|
let userIds: string[] = [];
|
||||||
|
|
||||||
|
if (data.isPublic) {
|
||||||
|
const users = await em.find(User, {}, { fields: ['id'] });
|
||||||
|
userIds = users.map((u) => u.id);
|
||||||
|
} else if (data.userIds && data.userIds.length > 0) {
|
||||||
|
userIds = data.userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
this.logger.warn('No users found for announcement');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcementId = data.announcementId;
|
||||||
|
this.logger.debug(`Sending announcement to ${userIds.length} users`);
|
||||||
|
|
||||||
|
const userAnnouncements = userIds.map((userId) =>
|
||||||
|
em.create(UserAnnouncement, {
|
||||||
|
user: em.getReference(User, userId),
|
||||||
|
announcement: em.getReference(Announcement, announcementId),
|
||||||
|
isRead: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await em.persistAndFlush(userAnnouncements);
|
||||||
|
|
||||||
|
this.logger.log(`Successfully sent announcement ${announcementId} to ${userIds.length} users`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishAnnouncement(job: Job): void {
|
||||||
|
this.logger.log(`Publishing announcement: ${job.data.announcementId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnWorkerEvent('completed')
|
||||||
|
onCompleted(job: Job) {
|
||||||
|
this.logger.log(`Announcement job ${job.id} completed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnWorkerEvent('failed')
|
||||||
|
onFailed(job: Job, error: Error) {
|
||||||
|
this.logger.error(`Announcement job ${job.id} failed:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { Announcement } from '../entities/announcement.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnnouncementRepository extends EntityRepository<Announcement> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, Announcement);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { UserAnnouncement } from '../entities/user-announcement.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserAnnouncementRepository extends EntityRepository<UserAnnouncement> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, UserAnnouncement);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user