feat: Complete NestJS backend scaffold — 22 modules, 39 entities, WebSocket gateway
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Full backend rewrite from Rust/Axum to NestJS/TypeScript. - 22 feature modules (auth, servers, wipes, maps, plugins, players, console, chat, team, notifications, settings, schedules, analytics, alerts, status, store, webstore, admin, setup, migration, users, licenses) - 39 TypeORM entities matching PostgreSQL schema (12 migrations) - Common infrastructure: JWT/RBAC guards, decorators, exception filter - NATS service with pub/sub/request-reply - Socket.IO WebSocket gateway with NATS bridge - Docker: NestJS Dockerfile + updated docker-compose.yml - Zero compile errors (npx tsc --noEmit clean) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
backend-nest/src/modules/schedules/dto/create-task.dto.ts
Normal file
54
backend-nest/src/modules/schedules/dto/create-task.dto.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { IsString, IsEnum, IsOptional, IsObject, Matches } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export enum TaskType {
|
||||
RESTART = 'restart',
|
||||
ANNOUNCEMENT = 'announcement',
|
||||
COMMAND = 'command',
|
||||
PLUGIN_RELOAD = 'plugin_reload',
|
||||
}
|
||||
|
||||
export class CreateTaskDto {
|
||||
@ApiProperty({
|
||||
description: 'Type of scheduled task',
|
||||
enum: TaskType,
|
||||
example: 'restart',
|
||||
})
|
||||
@IsEnum(TaskType)
|
||||
task_type: TaskType;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Name of the task',
|
||||
example: 'Daily restart',
|
||||
})
|
||||
@IsString()
|
||||
task_name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Cron expression (e.g., "0 0 * * *" for daily at midnight)',
|
||||
example: '0 0 * * *',
|
||||
})
|
||||
@IsString()
|
||||
@Matches(/^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([01]?\d|2\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|[0-6])$/, {
|
||||
message: 'Invalid cron expression format',
|
||||
})
|
||||
cron_expression: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Timezone for the schedule (IANA timezone)',
|
||||
example: 'America/New_York',
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
timezone?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Task-specific configuration object',
|
||||
example: { message: 'Server restarting in 5 minutes', countdown: 300 },
|
||||
required: false,
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
task_config?: Record<string, any>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateTaskDto } from './create-task.dto';
|
||||
|
||||
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
|
||||
103
backend-nest/src/modules/schedules/schedules.controller.ts
Normal file
103
backend-nest/src/modules/schedules/schedules.controller.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentTenant } from '../../common/decorators/current-tenant.decorator';
|
||||
import { SchedulesService } from './schedules.service';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
import { UpdateTaskDto } from './dto/update-task.dto';
|
||||
|
||||
@ApiTags('schedules')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('schedules')
|
||||
export class SchedulesController {
|
||||
constructor(private readonly schedulesService: SchedulesService) {}
|
||||
|
||||
@Get('tasks')
|
||||
@ApiOperation({
|
||||
summary: 'Get all scheduled tasks',
|
||||
description: 'Returns all scheduled tasks for this license',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Tasks retrieved successfully',
|
||||
})
|
||||
async getTasks(@CurrentTenant() licenseId: string) {
|
||||
return await this.schedulesService.getTasks(licenseId);
|
||||
}
|
||||
|
||||
@Post('tasks')
|
||||
@ApiOperation({
|
||||
summary: 'Create a scheduled task',
|
||||
description: 'Create a new scheduled task (restart, announcement, command, or plugin reload)',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Task created successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Invalid cron expression',
|
||||
})
|
||||
async createTask(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Body() dto: CreateTaskDto,
|
||||
) {
|
||||
return await this.schedulesService.createTask(licenseId, dto);
|
||||
}
|
||||
|
||||
@Put('tasks/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Update a scheduled task',
|
||||
description: 'Update task configuration, schedule, or settings',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Task updated successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Task not found',
|
||||
})
|
||||
async updateTask(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') taskId: string,
|
||||
@Body() dto: UpdateTaskDto,
|
||||
) {
|
||||
return await this.schedulesService.updateTask(licenseId, taskId, dto);
|
||||
}
|
||||
|
||||
@Delete('tasks/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Delete a scheduled task',
|
||||
description: 'Remove a scheduled task and unregister from scheduler',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Task deleted successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Task not found',
|
||||
})
|
||||
async deleteTask(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') taskId: string,
|
||||
) {
|
||||
return await this.schedulesService.deleteTask(licenseId, taskId);
|
||||
}
|
||||
}
|
||||
13
backend-nest/src/modules/schedules/schedules.module.ts
Normal file
13
backend-nest/src/modules/schedules/schedules.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SchedulesController } from './schedules.controller';
|
||||
import { SchedulesService } from './schedules.service';
|
||||
import { ScheduledTask } from '../../entities/scheduled-task.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ScheduledTask])],
|
||||
controllers: [SchedulesController],
|
||||
providers: [SchedulesService],
|
||||
exports: [SchedulesService],
|
||||
})
|
||||
export class SchedulesModule {}
|
||||
125
backend-nest/src/modules/schedules/schedules.service.ts
Normal file
125
backend-nest/src/modules/schedules/schedules.service.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ScheduledTask } from '../../entities/scheduled-task.entity';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
import { UpdateTaskDto } from './dto/update-task.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
constructor(
|
||||
@InjectRepository(ScheduledTask)
|
||||
private taskRepository: Repository<ScheduledTask>,
|
||||
) {}
|
||||
|
||||
async getTasks(licenseId: string): Promise<ScheduledTask[]> {
|
||||
return await this.taskRepository.find({
|
||||
where: { license_id: licenseId },
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async createTask(
|
||||
licenseId: string,
|
||||
dto: CreateTaskDto,
|
||||
): Promise<ScheduledTask> {
|
||||
// Validate cron expression is parseable
|
||||
// In production, you'd use a cron parser library to validate
|
||||
// For now, we rely on the regex in the DTO
|
||||
|
||||
// Set default timezone if not provided
|
||||
const timezone = dto.timezone || 'UTC';
|
||||
|
||||
const task = this.taskRepository.create({
|
||||
license_id: licenseId,
|
||||
task_type: dto.task_type,
|
||||
task_name: dto.task_name,
|
||||
cron_expression: dto.cron_expression,
|
||||
timezone: timezone,
|
||||
task_config: dto.task_config || {},
|
||||
is_enabled: true,
|
||||
last_run: null,
|
||||
next_run: null, // Would be calculated by scheduler
|
||||
created_at: new Date(),
|
||||
});
|
||||
|
||||
const saved = await this.taskRepository.save(task);
|
||||
|
||||
// TODO: Register task with scheduler (tokio-cron-scheduler in Rust)
|
||||
// This would send a NATS message to the scheduler service to register the task
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
licenseId: string,
|
||||
taskId: string,
|
||||
dto: UpdateTaskDto,
|
||||
): Promise<ScheduledTask> {
|
||||
const task = await this.taskRepository.findOne({
|
||||
where: {
|
||||
id: taskId,
|
||||
license_id: licenseId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException(`Scheduled task ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Update fields
|
||||
Object.assign(task, dto);
|
||||
|
||||
const updated = await this.taskRepository.save(task);
|
||||
|
||||
// TODO: Update task registration with scheduler
|
||||
// Send NATS message to update the task in tokio-cron-scheduler
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteTask(licenseId: string, taskId: string) {
|
||||
const task = await this.taskRepository.findOne({
|
||||
where: {
|
||||
id: taskId,
|
||||
license_id: licenseId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException(`Scheduled task ${taskId} not found`);
|
||||
}
|
||||
|
||||
await this.taskRepository.delete(taskId);
|
||||
|
||||
// TODO: Unregister task from scheduler
|
||||
// Send NATS message to remove the task from tokio-cron-scheduler
|
||||
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
async toggleTask(licenseId: string, taskId: string, enabled: boolean) {
|
||||
const task = await this.taskRepository.findOne({
|
||||
where: {
|
||||
id: taskId,
|
||||
license_id: licenseId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException(`Scheduled task ${taskId} not found`);
|
||||
}
|
||||
|
||||
task.is_enabled = enabled;
|
||||
const updated = await this.taskRepository.save(task);
|
||||
|
||||
// TODO: Enable/disable task in scheduler
|
||||
// Send NATS message to pause or resume the task
|
||||
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user