fix: Align NestJS entities with actual DB schema — 12 files, 5 entities
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

Root cause of all remaining 500s: TypeORM entities were scaffolded with
"ideal" column names that don't match the Postgres columns created by
the Rust migrations. Every query generated SQL referencing non-existent
columns.

Entity fixes:
- notifications_config: email_enabled→email_alerts_enabled, removed
  6 phantom columns (email_address, notify_on_start, notify_on_stop,
  notify_on_player_threshold, player_threshold), renamed 4 notify
  columns to match DB (notify_server_crash, notify_wipe_start, etc),
  added 3 missing columns (notify_server_offline, notify_store_purchase,
  notify_player_report)
- team_members: joined_at→accepted_at (nullable, matches DB)
- roles: removed description column (doesn't exist in DB)
- scheduled_tasks: is_enabled→is_active, removed phantom last_run
- wipe_profiles: pre/post_wipe_config nullable→NOT NULL with default

Service/DTO fixes:
- Updated all property references across notifications, team, schedules
  services and DTOs to match corrected entity names
- Added is_active to UpdateTaskDto (frontend sends it, was being
  rejected by forbidNonWhitelisted validation)
- Removed description from CreateRoleDto

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-15 22:33:55 -05:00
parent 3cb714a792
commit 78e97babf1
12 changed files with 78 additions and 110 deletions

View File

@@ -21,35 +21,29 @@ export class NotificationsConfig {
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
pushbullet_enabled: boolean; pushbullet_enabled: boolean;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: true })
email_enabled: boolean; email_alerts_enabled: boolean;
@Column({ type: 'text', nullable: true })
email_address: string | null;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_start: boolean; notify_wipe_start: boolean;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_stop: boolean; notify_wipe_complete: boolean;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_crash: boolean; notify_wipe_failed: boolean;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_wipe_start: boolean; notify_server_crash: boolean;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_wipe_complete: boolean; notify_server_offline: boolean;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
notify_on_wipe_failure: boolean; notify_store_purchase: boolean;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
notify_on_player_threshold: boolean; notify_player_report: boolean;
@Column({ type: 'int', nullable: true })
player_threshold: number | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' }) @Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date; created_at: Date;

View File

@@ -12,9 +12,6 @@ export class Role {
@Column({ type: 'varchar', length: 50 }) @Column({ type: 'varchar', length: 50 })
role_name: string; role_name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
is_system_default: boolean; is_system_default: boolean;

View File

@@ -26,10 +26,7 @@ export class ScheduledTask {
task_config: Record<string, any>; task_config: Record<string, any>;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
is_enabled: boolean; is_active: boolean;
@Column({ type: 'timestamptz', nullable: true })
last_run: Date | null;
@Column({ type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
next_run: Date | null; next_run: Date | null;

View File

@@ -21,8 +21,8 @@ export class TeamMember {
@Column({ type: 'uuid' }) @Column({ type: 'uuid' })
invited_by: string; invited_by: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' }) @Column({ type: 'timestamptz', nullable: true })
joined_at: Date; accepted_at: Date | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' }) @Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date; created_at: Date;

View File

@@ -15,11 +15,11 @@ export class WipeProfile {
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
description: string | null; description: string | null;
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', default: {} })
pre_wipe_config: Record<string, any> | null; pre_wipe_config: Record<string, any>;
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', default: {} })
post_wipe_config: Record<string, any> | null; post_wipe_config: Record<string, any>;
@Column({ type: 'timestamptz', default: () => 'NOW()' }) @Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date; created_at: Date;

View File

@@ -73,7 +73,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
relations: ['license', 'role'], relations: ['license', 'role'],
}); });
if (teamMember && teamMember.joined_at) { if (teamMember && teamMember.accepted_at) {
license = teamMember.license; license = teamMember.license;
role = teamMember.role; role = teamMember.role;
} }

View File

@@ -21,24 +21,6 @@ export class UpdateConfigDto {
@IsOptional() @IsOptional()
discord_webhook_url?: string; discord_webhook_url?: string;
@ApiProperty({
description: 'Enable email notifications',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
email_enabled?: boolean;
@ApiProperty({
description: 'Email address for notifications',
example: 'admin@example.com',
required: false,
})
@IsString()
@IsOptional()
email_address?: string;
@ApiProperty({ @ApiProperty({
description: 'Enable Pushbullet notifications', description: 'Enable Pushbullet notifications',
example: false, example: false,
@@ -58,31 +40,13 @@ export class UpdateConfigDto {
pushbullet_api_key?: string; pushbullet_api_key?: string;
@ApiProperty({ @ApiProperty({
description: 'Notify on server start', description: 'Enable email alert notifications',
example: true, example: true,
required: false, required: false,
}) })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
notify_on_start?: boolean; email_alerts_enabled?: boolean;
@ApiProperty({
description: 'Notify on server stop',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
notify_on_stop?: boolean;
@ApiProperty({
description: 'Notify on server crash',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
notify_on_crash?: boolean;
@ApiProperty({ @ApiProperty({
description: 'Notify on wipe start', description: 'Notify on wipe start',
@@ -91,7 +55,7 @@ export class UpdateConfigDto {
}) })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
notify_on_wipe_start?: boolean; notify_wipe_start?: boolean;
@ApiProperty({ @ApiProperty({
description: 'Notify on wipe complete', description: 'Notify on wipe complete',
@@ -100,7 +64,7 @@ export class UpdateConfigDto {
}) })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
notify_on_wipe_complete?: boolean; notify_wipe_complete?: boolean;
@ApiProperty({ @ApiProperty({
description: 'Notify on wipe failure', description: 'Notify on wipe failure',
@@ -109,23 +73,41 @@ export class UpdateConfigDto {
}) })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
notify_on_wipe_failure?: boolean; notify_wipe_failed?: boolean;
@ApiProperty({ @ApiProperty({
description: 'Notify on player count threshold', description: 'Notify on server crash',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
notify_server_crash?: boolean;
@ApiProperty({
description: 'Notify when server goes offline',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
notify_server_offline?: boolean;
@ApiProperty({
description: 'Notify on store purchase',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
notify_store_purchase?: boolean;
@ApiProperty({
description: 'Notify on player report',
example: false, example: false,
required: false, required: false,
}) })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
notify_on_player_threshold?: boolean; notify_player_report?: boolean;
@ApiProperty({
description: 'Player count threshold',
example: '100',
required: false,
})
@IsString()
@IsOptional()
player_threshold?: string;
} }

View File

@@ -22,18 +22,16 @@ export class NotificationsService {
license_id: licenseId, license_id: licenseId,
discord_enabled: false, discord_enabled: false,
discord_webhook_url: null, discord_webhook_url: null,
email_enabled: false,
email_address: null,
pushbullet_enabled: false, pushbullet_enabled: false,
pushbullet_api_key: null, pushbullet_api_key: null,
notify_on_start: true, email_alerts_enabled: true,
notify_on_stop: true, notify_wipe_start: true,
notify_on_crash: true, notify_wipe_complete: true,
notify_on_wipe_start: true, notify_wipe_failed: true,
notify_on_wipe_complete: true, notify_server_crash: true,
notify_on_wipe_failure: true, notify_server_offline: true,
notify_on_player_threshold: false, notify_store_purchase: true,
player_threshold: null, notify_player_report: false,
}); });
config = await this.configRepository.save(config); config = await this.configRepository.save(config);

View File

@@ -1,4 +1,14 @@
import { PartialType } from '@nestjs/swagger'; import { PartialType, ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';
import { CreateTaskDto } from './create-task.dto'; import { CreateTaskDto } from './create-task.dto';
export class UpdateTaskDto extends PartialType(CreateTaskDto) {} export class UpdateTaskDto extends PartialType(CreateTaskDto) {
@ApiProperty({
description: 'Enable or disable the task',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
is_active?: boolean;
}

View File

@@ -41,8 +41,7 @@ export class SchedulesService {
cron_expression: dto.cron_expression, cron_expression: dto.cron_expression,
timezone: timezone, timezone: timezone,
task_config: dto.task_config || {}, task_config: dto.task_config || {},
is_enabled: true, is_active: true,
last_run: null,
next_run: null, // Would be calculated by scheduler next_run: null, // Would be calculated by scheduler
created_at: new Date(), created_at: new Date(),
}); });
@@ -114,7 +113,7 @@ export class SchedulesService {
throw new NotFoundException(`Scheduled task ${taskId} not found`); throw new NotFoundException(`Scheduled task ${taskId} not found`);
} }
task.is_enabled = enabled; task.is_active = enabled;
const updated = await this.taskRepository.save(task); const updated = await this.taskRepository.save(task);
// TODO: Enable/disable task in scheduler // TODO: Enable/disable task in scheduler

View File

@@ -1,4 +1,4 @@
import { IsString, IsObject, IsOptional } from 'class-validator'; import { IsString, IsObject } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class CreateRoleDto { export class CreateRoleDto {
@@ -21,12 +21,4 @@ export class CreateRoleDto {
@IsObject() @IsObject()
permissions: Record<string, any>; permissions: Record<string, any>;
@ApiProperty({
description: 'Optional role description',
example: 'Custom role for moderators with limited permissions',
required: false,
})
@IsString()
@IsOptional()
description?: string;
} }

View File

@@ -29,7 +29,7 @@ export class TeamService {
const members = await this.teamMemberRepository.find({ const members = await this.teamMemberRepository.find({
where: { license_id: licenseId }, where: { license_id: licenseId },
relations: ['user', 'role'], relations: ['user', 'role'],
order: { joined_at: 'DESC' }, order: { accepted_at: 'DESC' },
}); });
// Get all roles (system defaults + custom roles for this license) // Get all roles (system defaults + custom roles for this license)
@@ -43,7 +43,7 @@ export class TeamService {
email: member.user?.email, email: member.user?.email,
role_id: member.role_id, role_id: member.role_id,
role_name: member.role?.role_name, role_name: member.role?.role_name,
joined_at: member.joined_at, accepted_at: member.accepted_at,
invited_by: member.invited_by, invited_by: member.invited_by,
})), })),
roles, roles,
@@ -101,7 +101,7 @@ export class TeamService {
user_id: user.id, user_id: user.id,
role_id: dto.role_id, role_id: dto.role_id,
invited_by: invitedBy, invited_by: invitedBy,
joined_at: new Date(), accepted_at: new Date(),
}); });
const saved = await this.teamMemberRepository.save(teamMember); const saved = await this.teamMemberRepository.save(teamMember);
@@ -123,7 +123,7 @@ export class TeamService {
email: memberWithData.user?.email, email: memberWithData.user?.email,
role_id: memberWithData.role_id, role_id: memberWithData.role_id,
role_name: memberWithData.role?.role_name, role_name: memberWithData.role?.role_name,
joined_at: memberWithData.joined_at, accepted_at: memberWithData.accepted_at,
invited_by: memberWithData.invited_by, invited_by: memberWithData.invited_by,
}; };
} }
@@ -177,7 +177,6 @@ export class TeamService {
license_id: licenseId, license_id: licenseId,
role_name: dto.role_name, role_name: dto.role_name,
permissions: dto.permissions, permissions: dto.permissions,
description: dto.description,
is_system_default: false, is_system_default: false,
}); });