fix: Align NestJS entities with actual DB schema — 12 files, 5 entities
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user