feat: add notification data model and migration

This commit is contained in:
Bastian Wagner
2026-08-04 18:26:50 +02:00
parent ecfa847d2a
commit 7410672630
5 changed files with 166 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddNotificationTables1785600000000 implements MigrationInterface {
name = 'AddNotificationTables1785600000000';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE "notification_recipient"`);
await queryRunner.query(`DROP TABLE "notification"`);
}
}

View File

@@ -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"']);
});
});

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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',
];