diff --git a/myteamwallet_backend/src/database/migrations/1785600000000-AddNotificationTables.ts b/myteamwallet_backend/src/database/migrations/1785600000000-AddNotificationTables.ts new file mode 100644 index 0000000..c8c0329 --- /dev/null +++ b/myteamwallet_backend/src/database/migrations/1785600000000-AddNotificationTables.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNotificationTables1785600000000 implements MigrationInterface { + name = 'AddNotificationTables1785600000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "notification" ( + "id" SERIAL NOT NULL, + "teamId" integer NOT NULL, + "event" character varying NOT NULL, + "actorUserId" integer NOT NULL, + "payload" text NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_notification_id" PRIMARY KEY ("id") + ) + `); + await queryRunner.query( + `CREATE INDEX "IDX_notification_team_id" ON "notification" ("teamId")`, + ); + await queryRunner.query(` + ALTER TABLE "notification" + ADD CONSTRAINT "FK_notification_team" + FOREIGN KEY ("teamId") REFERENCES "team"("id") + ON DELETE CASCADE + `); + + await queryRunner.query(` + CREATE TABLE "notification_recipient" ( + "id" SERIAL NOT NULL, + "notificationId" integer NOT NULL, + "userId" integer NOT NULL, + "read" boolean NOT NULL DEFAULT false, + "readAt" TIMESTAMP, + CONSTRAINT "PK_notification_recipient_id" PRIMARY KEY ("id") + ) + `); + await queryRunner.query( + `CREATE INDEX "IDX_notification_recipient_notification_id" ON "notification_recipient" ("notificationId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_notification_recipient_user_id" ON "notification_recipient" ("userId")`, + ); + await queryRunner.query(` + ALTER TABLE "notification_recipient" + ADD CONSTRAINT "FK_notification_recipient_notification" + FOREIGN KEY ("notificationId") REFERENCES "notification"("id") + ON DELETE CASCADE + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "notification_recipient"`); + await queryRunner.query(`DROP TABLE "notification"`); + } +} diff --git a/myteamwallet_backend/src/database/migrations/AddNotificationTables.spec.ts b/myteamwallet_backend/src/database/migrations/AddNotificationTables.spec.ts new file mode 100644 index 0000000..e50f1ea --- /dev/null +++ b/myteamwallet_backend/src/database/migrations/AddNotificationTables.spec.ts @@ -0,0 +1,38 @@ +describe('AddNotificationTables1785600000000', () => { + it('creates the notification and notification_recipient tables with their indexes and foreign keys', async () => { + const migrationModule = require('./1785600000000-AddNotificationTables'); + const migration = new migrationModule.AddNotificationTables1785600000000(); + const queryRunner = { query: jest.fn() } as any; + + await migration.up(queryRunner); + + const calls: string[] = queryRunner.query.mock.calls.map((c: any) => c[0]); + expect(calls).toHaveLength(7); + expect(calls.some((sql) => sql.includes('CREATE TABLE "notification"'))).toBe(true); + expect(calls.some((sql) => sql.includes('CREATE TABLE "notification_recipient"'))).toBe( + true, + ); + expect(calls.some((sql) => sql.includes('IDX_notification_team_id'))).toBe(true); + expect( + calls.some((sql) => sql.includes('IDX_notification_recipient_notification_id')), + ).toBe(true); + expect(calls.some((sql) => sql.includes('IDX_notification_recipient_user_id'))).toBe( + true, + ); + expect(calls.some((sql) => sql.includes('FK_notification_team'))).toBe(true); + expect(calls.some((sql) => sql.includes('FK_notification_recipient_notification'))).toBe( + true, + ); + }); + + it('drops both tables on down, recipient first to respect the foreign key', async () => { + const migrationModule = require('./1785600000000-AddNotificationTables'); + const migration = new migrationModule.AddNotificationTables1785600000000(); + const queryRunner = { query: jest.fn() } as any; + + await migration.down(queryRunner); + + const calls: string[] = queryRunner.query.mock.calls.map((c: any) => c[0]); + expect(calls).toEqual(['DROP TABLE "notification_recipient"', 'DROP TABLE "notification"']); + }); +}); diff --git a/myteamwallet_backend/src/notifications/entities/notification-recipient.entity.ts b/myteamwallet_backend/src/notifications/entities/notification-recipient.entity.ts new file mode 100644 index 0000000..9c2fde0 --- /dev/null +++ b/myteamwallet_backend/src/notifications/entities/notification-recipient.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { EntityHelper } from 'src/utils/entity-helper'; +import { Notification } from './notification.entity'; + +@Entity() +export class NotificationRecipient extends EntityHelper { + @PrimaryGeneratedColumn() + id: number; + + @Index('IDX_notification_recipient_notification_id') + @ManyToOne(() => Notification, { onDelete: 'CASCADE' }) + notification: Notification; + + @Index('IDX_notification_recipient_user_id') + @Column() + userId: number; + + @Column({ default: false }) + read: boolean; + + @Column({ nullable: true }) + readAt: Date | null; +} diff --git a/myteamwallet_backend/src/notifications/entities/notification.entity.ts b/myteamwallet_backend/src/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..510b0bb --- /dev/null +++ b/myteamwallet_backend/src/notifications/entities/notification.entity.ts @@ -0,0 +1,33 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { EntityHelper } from 'src/utils/entity-helper'; +import { Team } from 'src/teams/entities/team.entity'; +import { NOTIFICATION_EVENT } from '../model/notification-event.type'; + +@Entity() +export class Notification extends EntityHelper { + @PrimaryGeneratedColumn() + id: number; + + @Index('IDX_notification_team_id') + @ManyToOne(() => Team, { eager: false }) + team: Team; + + @Column() + event: NOTIFICATION_EVENT; + + @Column() + actorUserId: number; + + @Column({ type: 'text' }) + payload: string; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/myteamwallet_backend/src/notifications/model/notification-event.type.ts b/myteamwallet_backend/src/notifications/model/notification-event.type.ts new file mode 100644 index 0000000..9522fff --- /dev/null +++ b/myteamwallet_backend/src/notifications/model/notification-event.type.ts @@ -0,0 +1,16 @@ +export type NOTIFICATION_EVENT = + | 'player_active_update' + | 'player_team_role_update' + | 'player_creation' + | 'public_access_enabled' + | 'public_access_rotated' + | 'user_invite_link_create'; + +export const NOTIFICATION_EVENT_VALUES: NOTIFICATION_EVENT[] = [ + 'player_active_update', + 'player_team_role_update', + 'player_creation', + 'public_access_enabled', + 'public_access_rotated', + 'user_invite_link_create', +];