Skip to content

Commit

Permalink
General REST behavior for jobs (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonloeser authored Jun 9, 2024
1 parent 2c89a65 commit 99aad7f
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 25 deletions.
10 changes: 6 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import {Transport} from '@nestjs/microservices';
import {GameModule} from './game/game.module';
import {MemberModule} from './member/member.module';
import {EmpireModule} from './empire/empire.module';
import { PresetsModule } from './presets/presets.module';
import {PresetsModule} from './presets/presets.module';
import {SystemModule} from "./system/system.module";
import { GameLogicModule } from './game-logic/game-logic.module';
import {GameLogicModule} from './game-logic/game-logic.module';
import {IncomingMessage} from 'http';
import {AuthService} from './auth/auth.service';
import {FriendsModule} from "./friend/friend.module";
import {FriendModule} from "./friend/friend.module";
import {JobModule} from "./job/job.module";

@Module({
imports: [
Expand Down Expand Up @@ -53,11 +54,12 @@ import {FriendsModule} from "./friend/friend.module";
}),
AuthModule,
UserModule,
FriendsModule,
FriendModule,
GameModule,
MemberModule,
SystemModule,
EmpireModule,
JobModule,
AchievementSummaryModule,
AchievementModule,
PresetsModule,
Expand Down
18 changes: 9 additions & 9 deletions src/friend/friend.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ import {NotFound, ObjectIdPipe} from '@mean-stream/nestx';
import {Types} from 'mongoose';
import {Validated} from '../util/validated.decorator';
import {Throttled} from '../util/throttled.decorator';
import {FriendsService} from './friend.service';
import {Auth, AuthUser} from '../auth/auth.decorator';
import {Friend} from './friend.schema';
import {FriendStatus, UpdateFriendDto} from './friend.dto';
import {User} from '../user/user.schema';
import {UniqueConflict} from '../util/unique-conflict.decorator';
import {FriendService} from "./friend.service";

@Controller('users/:from/friends')
@ApiTags('Friends')
@Validated()
@Throttled()
export class FriendsController {
export class FriendController {
constructor(
private readonly friendsService: FriendsService,
private readonly friendService: FriendService,
) {
}

Expand All @@ -60,7 +60,7 @@ export class FriendsController {
if (!from.equals(user._id)) {
throw new ForbiddenException('You can only access your own friends list.');
}
return this.friendsService.getFriends(from, status as FriendStatus);
return this.friendService.getFriends(from, status as FriendStatus);
}

@Put(':to')
Expand All @@ -81,11 +81,11 @@ export class FriendsController {
if (from.equals(to)) {
throw new ConflictException('You cannot send a friend request to yourself.');
}
const existingRequest = await this.friendsService.findOne({$or: [{from, to}, {from: to, to: from}]});
const existingRequest = await this.friendService.findOne({$or: [{from, to}, {from: to, to: from}]});
if (existingRequest) {
throw new ConflictException('Friend request already exists.');
}
return this.friendsService.create({from, to, status: 'requested'});
return this.friendService.create({from, to, status: 'requested'});
}

@Patch(':to')
Expand All @@ -106,7 +106,7 @@ export class FriendsController {
if (!to.equals(user._id)) {
throw new ForbiddenException('You can only accept friend requests to your own account.');
}
return this.friendsService.acceptFriendRequest(to, from, dto);
return this.friendService.acceptFriendRequest(to, from, dto);
}

@Delete(':to')
Expand All @@ -125,8 +125,8 @@ export class FriendsController {
if (!from.equals(user._id) && !to.equals(user._id)) {
throw new ForbiddenException('You can only delete friends from or to your own account.');
}
const deleted = await this.friendsService.deleteOne({from, to});
const inverse = await this.friendsService.deleteOne({from: to, to: from});
const deleted = await this.friendService.deleteOne({from, to});
const inverse = await this.friendService.deleteOne({from: to, to: from});
return deleted || inverse;
}
}
8 changes: 4 additions & 4 deletions src/friend/friend.handler.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {Injectable} from "@nestjs/common";
import {FriendsService} from "./friend.service";
import {OnEvent} from "@nestjs/event-emitter";
import {User} from "../user/user.schema";
import {FriendService} from "./friend.service";

@Injectable()
export class FriendsHandler {
export class FriendHandler {
constructor(
private friendsService: FriendsService,
private friendService: FriendService,
) {
}

@OnEvent('users.*.deleted')
async onUserDeleted(user: User): Promise<void> {
await this.friendsService.deleteMany({$or: [{from: user._id}, {to: user._id}]});
await this.friendService.deleteMany({$or: [{from: user._id}, {to: user._id}]});
}
}
14 changes: 7 additions & 7 deletions src/friend/friend.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {Module} from '@nestjs/common';
import {MongooseModule} from '@nestjs/mongoose';
import {Friend, FriendSchema} from "./friend.schema";
import {FriendsService} from "./friend.service";
import {FriendsController} from "./friend.controller";
import {FriendsHandler} from "./friend.handler";
import {FriendService} from "./friend.service";
import {FriendHandler} from "./friend.handler";
import {FriendController} from "./friend.controller";

@Module({
imports: [
MongooseModule.forFeature([{name: Friend.name, schema: FriendSchema}]),
],
providers: [FriendsService, FriendsHandler],
controllers: [FriendsController],
exports: [FriendsService],
providers: [FriendService, FriendHandler],
controllers: [FriendController],
exports: [FriendService],
})
export class FriendsModule {
export class FriendModule {
}
2 changes: 1 addition & 1 deletion src/friend/friend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {FriendStatus, UpdateFriendDto} from './friend.dto';

@Injectable()
@EventRepository()
export class FriendsService extends MongooseRepository<Friend> {
export class FriendService extends MongooseRepository<Friend> {
constructor(
@InjectModel(Friend.name) private friendModel: Model<Friend>,
private eventEmitter: EventService,
Expand Down
6 changes: 6 additions & 0 deletions src/job/job-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum JobType {
BUILDING = 'building',
DISTRICT = 'district',
UPGRADE = 'upgrade',
TECHNOLOGY = 'technology',
}
114 changes: 114 additions & 0 deletions src/job/job.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
Body,
Controller,
Delete, ForbiddenException,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import {
ApiCreatedResponse,
ApiForbiddenResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import {Types} from 'mongoose';
import {NotFound, ObjectIdPipe} from '@mean-stream/nestx';
import {Validated} from '../util/validated.decorator';
import {Throttled} from '../util/throttled.decorator';
import {Auth, AuthUser} from '../auth/auth.decorator';
import {Job} from './job.schema';
import {User} from '../user/user.schema';
import {CreateJobDto} from './job.dto';
import {JobService} from './job.service';
import {EmpireService} from "../empire/empire.service";
import {JobType} from "./job-type.enum";

@Controller('games/:game/empires/:empire/jobs')
@ApiTags('Jobs')
@Validated()
@Throttled()
export class JobController {
constructor(
private readonly jobService: JobService,
private readonly empireService: EmpireService,
) {
}

@Get()
@Auth()
@ApiOperation({description: 'Get the job list with optional filters for system and type.'})
@ApiOkResponse({type: [Job]})
@ApiForbiddenResponse({description: 'You can only access jobs for your own empire.'})
@NotFound()
@ApiQuery({
name: 'system',
description: 'Filter jobs by system',
required: false,
type: String,
example: '60d6f7eb8b4b8a001d6f7eb1',
})
@ApiQuery({
name: 'type',
description: 'Filter jobs by type (`building`, `district`, `upgrade`, `technology`).',
required: false,
enum: JobType,
})
async getJobs(
@Param('game', ObjectIdPipe) game: Types.ObjectId,
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
@AuthUser() user: User,
@Query('system', ObjectIdPipe) system?: Types.ObjectId,
@Query('type') type?: string,
): Promise<Job[]> {
const userEmpire = await this.empireService.findOne({game, user: user._id});
if (!userEmpire || !empire.equals(userEmpire._id)) {
throw new ForbiddenException('You can only access jobs for your own empire.');
}
// TODO: Return jobs with given filters
return Array.of(new Job());
}

@Post()
@Auth()
@ApiOperation({description: 'Create a new job for your empire.'})
@ApiCreatedResponse({type: Job})
@ApiForbiddenResponse({description: 'You can only create jobs for your own empire.'})
@NotFound()
async createJob(
@Param('game', ObjectIdPipe) game: Types.ObjectId,
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
@AuthUser() user: User,
@Body() createJobDto: CreateJobDto,
): Promise<Job> {
const userEmpire = await this.empireService.findOne({game, user: user._id});
if (!userEmpire || !empire.equals(userEmpire._id)) {
throw new ForbiddenException('You can only create jobs for your own empire.');
}
// TODO: Create job
return new Job();
}

@Delete(':id')
@Auth()
@ApiOperation({description: 'Delete a job from your empire.'})
@ApiOkResponse({type: Job})
@NotFound('Job not found.')
@ApiForbiddenResponse({description: 'You can only delete jobs from your own empire.'})
async deleteJob(
@Param('game', ObjectIdPipe) game: Types.ObjectId,
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
@Param('id', ObjectIdPipe) id: Types.ObjectId,
@AuthUser() user: User,
): Promise<Job | null> {
const userEmpire = await this.empireService.findOne({game, user: user._id});
if (!userEmpire || !empire.equals(userEmpire._id)) {
throw new ForbiddenException('You can only delete jobs for your own empire.');
}
// TODO: Delete job
return null;
}
}
5 changes: 5 additions & 0 deletions src/job/job.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {PickType} from '@nestjs/swagger';
import {Job} from "./job.schema";

export class CreateJobDto extends PickType(Job, ['system', 'type', 'building', 'district', 'technology'] as const) {
}
18 changes: 18 additions & 0 deletions src/job/job.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Module} from '@nestjs/common';
import {MongooseModule} from '@nestjs/mongoose';
import {Job, JobSchema} from './job.schema';
import {JobController} from "./job.controller";
import {JobService} from "./job.service";
import {EmpireModule} from "../empire/empire.module";

@Module({
imports: [
MongooseModule.forFeature([{name: Job.name, schema: JobSchema}]),
EmpireModule,
],
controllers: [JobController],
providers: [JobService],
exports: [JobService],
})
export class JobModule {
}
68 changes: 68 additions & 0 deletions src/job/job.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
import {Document, Types} from 'mongoose';
import {BUILDING_NAMES, BuildingName} from "../game-logic/buildings";
import {DISTRICT_NAMES, DistrictName} from "../game-logic/districts";
import {RESOURCES_SCHEMA_PROPERTIES, TECHNOLOGY_TAGS, TechnologyTag} from "../game-logic/types";
import {ResourceName} from "../game-logic/resources";
import {GLOBAL_SCHEMA_OPTIONS, GlobalSchema} from '../util/schema';
import {IsEnum, IsIn, IsNumber, IsOptional} from 'class-validator';
import {ApiProperty, ApiPropertyOptional} from '@nestjs/swagger';
import {OptionalRef, Ref} from "@mean-stream/nestx";
import {JobType} from "./job-type.enum";

export type JobDocument = Job & Document<Types.ObjectId>;

@Schema(GLOBAL_SCHEMA_OPTIONS)
export class Job extends GlobalSchema {
@Prop({required: true})
@ApiProperty({description: 'Current progress of the job'})
@IsNumber()
progress: number;

@Prop({required: true})
@ApiProperty({description: 'Total progress steps required for the job'})
@IsNumber()
total: number;

@Ref('Game')
game: Types.ObjectId;

@Ref('Empire')
empire: Types.ObjectId;

@OptionalRef('System')
system?: Types.ObjectId;

@Prop({required: true, type: String, enum: JobType})
@ApiProperty({enum: JobType, description: 'Type of the job'})
@IsEnum(JobType)
type: JobType;

@Prop({type: String})
@IsOptional()
@ApiPropertyOptional({required: false, description: 'Building name for the job'})
@IsIn(BUILDING_NAMES)
building?: BuildingName;

@Prop({type: String})
@IsOptional()
@ApiPropertyOptional({required: false, description: 'District name for the job'})
@IsIn(DISTRICT_NAMES)
district?: DistrictName;

@Prop({type: String})
@IsOptional()
@ApiPropertyOptional({required: false, description: 'Technology name for the job'})
@IsIn(TECHNOLOGY_TAGS)
technology?: TechnologyTag;

@Prop({type: Map, of: Number, default: {}})
@IsOptional()
@ApiPropertyOptional({
description: 'Initial cost of resources for the job',
...RESOURCES_SCHEMA_PROPERTIES,
})
cost?: Record<ResourceName, number>;
}

export const JobSchema = SchemaFactory.createForClass(Job);
16 changes: 16 additions & 0 deletions src/job/job.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Injectable} from "@nestjs/common";
import {InjectModel} from "@nestjs/mongoose";
import {Job} from "./job.schema";
import {Model} from "mongoose";
import {EventRepository, EventService, MongooseRepository} from "@mean-stream/nestx";

@Injectable()
@EventRepository()
export class JobService extends MongooseRepository<Job> {
constructor(
@InjectModel(Job.name) private jobModel: Model<Job>,
private eventEmitter: EventService,
) {
super(jobModel);
}
}

0 comments on commit 99aad7f

Please sign in to comment.