Skip to content

Commit 99aad7f

Browse files
authored
General REST behavior for jobs (#55)
1 parent 2c89a65 commit 99aad7f

11 files changed

+254
-25
lines changed

src/app.module.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {Transport} from '@nestjs/microservices';
1616
import {GameModule} from './game/game.module';
1717
import {MemberModule} from './member/member.module';
1818
import {EmpireModule} from './empire/empire.module';
19-
import { PresetsModule } from './presets/presets.module';
19+
import {PresetsModule} from './presets/presets.module';
2020
import {SystemModule} from "./system/system.module";
21-
import { GameLogicModule } from './game-logic/game-logic.module';
21+
import {GameLogicModule} from './game-logic/game-logic.module';
2222
import {IncomingMessage} from 'http';
2323
import {AuthService} from './auth/auth.service';
24-
import {FriendsModule} from "./friend/friend.module";
24+
import {FriendModule} from "./friend/friend.module";
25+
import {JobModule} from "./job/job.module";
2526

2627
@Module({
2728
imports: [
@@ -53,11 +54,12 @@ import {FriendsModule} from "./friend/friend.module";
5354
}),
5455
AuthModule,
5556
UserModule,
56-
FriendsModule,
57+
FriendModule,
5758
GameModule,
5859
MemberModule,
5960
SystemModule,
6061
EmpireModule,
62+
JobModule,
6163
AchievementSummaryModule,
6264
AchievementModule,
6365
PresetsModule,

src/friend/friend.controller.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ import {NotFound, ObjectIdPipe} from '@mean-stream/nestx';
2222
import {Types} from 'mongoose';
2323
import {Validated} from '../util/validated.decorator';
2424
import {Throttled} from '../util/throttled.decorator';
25-
import {FriendsService} from './friend.service';
2625
import {Auth, AuthUser} from '../auth/auth.decorator';
2726
import {Friend} from './friend.schema';
2827
import {FriendStatus, UpdateFriendDto} from './friend.dto';
2928
import {User} from '../user/user.schema';
3029
import {UniqueConflict} from '../util/unique-conflict.decorator';
30+
import {FriendService} from "./friend.service";
3131

3232
@Controller('users/:from/friends')
3333
@ApiTags('Friends')
3434
@Validated()
3535
@Throttled()
36-
export class FriendsController {
36+
export class FriendController {
3737
constructor(
38-
private readonly friendsService: FriendsService,
38+
private readonly friendService: FriendService,
3939
) {
4040
}
4141

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

6666
@Put(':to')
@@ -81,11 +81,11 @@ export class FriendsController {
8181
if (from.equals(to)) {
8282
throw new ConflictException('You cannot send a friend request to yourself.');
8383
}
84-
const existingRequest = await this.friendsService.findOne({$or: [{from, to}, {from: to, to: from}]});
84+
const existingRequest = await this.friendService.findOne({$or: [{from, to}, {from: to, to: from}]});
8585
if (existingRequest) {
8686
throw new ConflictException('Friend request already exists.');
8787
}
88-
return this.friendsService.create({from, to, status: 'requested'});
88+
return this.friendService.create({from, to, status: 'requested'});
8989
}
9090

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

112112
@Delete(':to')
@@ -125,8 +125,8 @@ export class FriendsController {
125125
if (!from.equals(user._id) && !to.equals(user._id)) {
126126
throw new ForbiddenException('You can only delete friends from or to your own account.');
127127
}
128-
const deleted = await this.friendsService.deleteOne({from, to});
129-
const inverse = await this.friendsService.deleteOne({from: to, to: from});
128+
const deleted = await this.friendService.deleteOne({from, to});
129+
const inverse = await this.friendService.deleteOne({from: to, to: from});
130130
return deleted || inverse;
131131
}
132132
}

src/friend/friend.handler.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import {Injectable} from "@nestjs/common";
2-
import {FriendsService} from "./friend.service";
32
import {OnEvent} from "@nestjs/event-emitter";
43
import {User} from "../user/user.schema";
4+
import {FriendService} from "./friend.service";
55

66
@Injectable()
7-
export class FriendsHandler {
7+
export class FriendHandler {
88
constructor(
9-
private friendsService: FriendsService,
9+
private friendService: FriendService,
1010
) {
1111
}
1212

1313
@OnEvent('users.*.deleted')
1414
async onUserDeleted(user: User): Promise<void> {
15-
await this.friendsService.deleteMany({$or: [{from: user._id}, {to: user._id}]});
15+
await this.friendService.deleteMany({$or: [{from: user._id}, {to: user._id}]});
1616
}
1717
}

src/friend/friend.module.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import {Module} from '@nestjs/common';
22
import {MongooseModule} from '@nestjs/mongoose';
33
import {Friend, FriendSchema} from "./friend.schema";
4-
import {FriendsService} from "./friend.service";
5-
import {FriendsController} from "./friend.controller";
6-
import {FriendsHandler} from "./friend.handler";
4+
import {FriendService} from "./friend.service";
5+
import {FriendHandler} from "./friend.handler";
6+
import {FriendController} from "./friend.controller";
77

88
@Module({
99
imports: [
1010
MongooseModule.forFeature([{name: Friend.name, schema: FriendSchema}]),
1111
],
12-
providers: [FriendsService, FriendsHandler],
13-
controllers: [FriendsController],
14-
exports: [FriendsService],
12+
providers: [FriendService, FriendHandler],
13+
controllers: [FriendController],
14+
exports: [FriendService],
1515
})
16-
export class FriendsModule {
16+
export class FriendModule {
1717
}

src/friend/friend.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {FriendStatus, UpdateFriendDto} from './friend.dto';
77

88
@Injectable()
99
@EventRepository()
10-
export class FriendsService extends MongooseRepository<Friend> {
10+
export class FriendService extends MongooseRepository<Friend> {
1111
constructor(
1212
@InjectModel(Friend.name) private friendModel: Model<Friend>,
1313
private eventEmitter: EventService,

src/job/job-type.enum.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum JobType {
2+
BUILDING = 'building',
3+
DISTRICT = 'district',
4+
UPGRADE = 'upgrade',
5+
TECHNOLOGY = 'technology',
6+
}

src/job/job.controller.ts

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
Body,
3+
Controller,
4+
Delete, ForbiddenException,
5+
Get,
6+
Param,
7+
Post,
8+
Query,
9+
} from '@nestjs/common';
10+
import {
11+
ApiCreatedResponse,
12+
ApiForbiddenResponse,
13+
ApiOkResponse,
14+
ApiOperation,
15+
ApiQuery,
16+
ApiTags,
17+
} from '@nestjs/swagger';
18+
import {Types} from 'mongoose';
19+
import {NotFound, ObjectIdPipe} from '@mean-stream/nestx';
20+
import {Validated} from '../util/validated.decorator';
21+
import {Throttled} from '../util/throttled.decorator';
22+
import {Auth, AuthUser} from '../auth/auth.decorator';
23+
import {Job} from './job.schema';
24+
import {User} from '../user/user.schema';
25+
import {CreateJobDto} from './job.dto';
26+
import {JobService} from './job.service';
27+
import {EmpireService} from "../empire/empire.service";
28+
import {JobType} from "./job-type.enum";
29+
30+
@Controller('games/:game/empires/:empire/jobs')
31+
@ApiTags('Jobs')
32+
@Validated()
33+
@Throttled()
34+
export class JobController {
35+
constructor(
36+
private readonly jobService: JobService,
37+
private readonly empireService: EmpireService,
38+
) {
39+
}
40+
41+
@Get()
42+
@Auth()
43+
@ApiOperation({description: 'Get the job list with optional filters for system and type.'})
44+
@ApiOkResponse({type: [Job]})
45+
@ApiForbiddenResponse({description: 'You can only access jobs for your own empire.'})
46+
@NotFound()
47+
@ApiQuery({
48+
name: 'system',
49+
description: 'Filter jobs by system',
50+
required: false,
51+
type: String,
52+
example: '60d6f7eb8b4b8a001d6f7eb1',
53+
})
54+
@ApiQuery({
55+
name: 'type',
56+
description: 'Filter jobs by type (`building`, `district`, `upgrade`, `technology`).',
57+
required: false,
58+
enum: JobType,
59+
})
60+
async getJobs(
61+
@Param('game', ObjectIdPipe) game: Types.ObjectId,
62+
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
63+
@AuthUser() user: User,
64+
@Query('system', ObjectIdPipe) system?: Types.ObjectId,
65+
@Query('type') type?: string,
66+
): Promise<Job[]> {
67+
const userEmpire = await this.empireService.findOne({game, user: user._id});
68+
if (!userEmpire || !empire.equals(userEmpire._id)) {
69+
throw new ForbiddenException('You can only access jobs for your own empire.');
70+
}
71+
// TODO: Return jobs with given filters
72+
return Array.of(new Job());
73+
}
74+
75+
@Post()
76+
@Auth()
77+
@ApiOperation({description: 'Create a new job for your empire.'})
78+
@ApiCreatedResponse({type: Job})
79+
@ApiForbiddenResponse({description: 'You can only create jobs for your own empire.'})
80+
@NotFound()
81+
async createJob(
82+
@Param('game', ObjectIdPipe) game: Types.ObjectId,
83+
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
84+
@AuthUser() user: User,
85+
@Body() createJobDto: CreateJobDto,
86+
): Promise<Job> {
87+
const userEmpire = await this.empireService.findOne({game, user: user._id});
88+
if (!userEmpire || !empire.equals(userEmpire._id)) {
89+
throw new ForbiddenException('You can only create jobs for your own empire.');
90+
}
91+
// TODO: Create job
92+
return new Job();
93+
}
94+
95+
@Delete(':id')
96+
@Auth()
97+
@ApiOperation({description: 'Delete a job from your empire.'})
98+
@ApiOkResponse({type: Job})
99+
@NotFound('Job not found.')
100+
@ApiForbiddenResponse({description: 'You can only delete jobs from your own empire.'})
101+
async deleteJob(
102+
@Param('game', ObjectIdPipe) game: Types.ObjectId,
103+
@Param('empire', ObjectIdPipe) empire: Types.ObjectId,
104+
@Param('id', ObjectIdPipe) id: Types.ObjectId,
105+
@AuthUser() user: User,
106+
): Promise<Job | null> {
107+
const userEmpire = await this.empireService.findOne({game, user: user._id});
108+
if (!userEmpire || !empire.equals(userEmpire._id)) {
109+
throw new ForbiddenException('You can only delete jobs for your own empire.');
110+
}
111+
// TODO: Delete job
112+
return null;
113+
}
114+
}

src/job/job.dto.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {PickType} from '@nestjs/swagger';
2+
import {Job} from "./job.schema";
3+
4+
export class CreateJobDto extends PickType(Job, ['system', 'type', 'building', 'district', 'technology'] as const) {
5+
}

src/job/job.module.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Module} from '@nestjs/common';
2+
import {MongooseModule} from '@nestjs/mongoose';
3+
import {Job, JobSchema} from './job.schema';
4+
import {JobController} from "./job.controller";
5+
import {JobService} from "./job.service";
6+
import {EmpireModule} from "../empire/empire.module";
7+
8+
@Module({
9+
imports: [
10+
MongooseModule.forFeature([{name: Job.name, schema: JobSchema}]),
11+
EmpireModule,
12+
],
13+
controllers: [JobController],
14+
providers: [JobService],
15+
exports: [JobService],
16+
})
17+
export class JobModule {
18+
}

src/job/job.schema.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
2+
import {Document, Types} from 'mongoose';
3+
import {BUILDING_NAMES, BuildingName} from "../game-logic/buildings";
4+
import {DISTRICT_NAMES, DistrictName} from "../game-logic/districts";
5+
import {RESOURCES_SCHEMA_PROPERTIES, TECHNOLOGY_TAGS, TechnologyTag} from "../game-logic/types";
6+
import {ResourceName} from "../game-logic/resources";
7+
import {GLOBAL_SCHEMA_OPTIONS, GlobalSchema} from '../util/schema';
8+
import {IsEnum, IsIn, IsNumber, IsOptional} from 'class-validator';
9+
import {ApiProperty, ApiPropertyOptional} from '@nestjs/swagger';
10+
import {OptionalRef, Ref} from "@mean-stream/nestx";
11+
import {JobType} from "./job-type.enum";
12+
13+
export type JobDocument = Job & Document<Types.ObjectId>;
14+
15+
@Schema(GLOBAL_SCHEMA_OPTIONS)
16+
export class Job extends GlobalSchema {
17+
@Prop({required: true})
18+
@ApiProperty({description: 'Current progress of the job'})
19+
@IsNumber()
20+
progress: number;
21+
22+
@Prop({required: true})
23+
@ApiProperty({description: 'Total progress steps required for the job'})
24+
@IsNumber()
25+
total: number;
26+
27+
@Ref('Game')
28+
game: Types.ObjectId;
29+
30+
@Ref('Empire')
31+
empire: Types.ObjectId;
32+
33+
@OptionalRef('System')
34+
system?: Types.ObjectId;
35+
36+
@Prop({required: true, type: String, enum: JobType})
37+
@ApiProperty({enum: JobType, description: 'Type of the job'})
38+
@IsEnum(JobType)
39+
type: JobType;
40+
41+
@Prop({type: String})
42+
@IsOptional()
43+
@ApiPropertyOptional({required: false, description: 'Building name for the job'})
44+
@IsIn(BUILDING_NAMES)
45+
building?: BuildingName;
46+
47+
@Prop({type: String})
48+
@IsOptional()
49+
@ApiPropertyOptional({required: false, description: 'District name for the job'})
50+
@IsIn(DISTRICT_NAMES)
51+
district?: DistrictName;
52+
53+
@Prop({type: String})
54+
@IsOptional()
55+
@ApiPropertyOptional({required: false, description: 'Technology name for the job'})
56+
@IsIn(TECHNOLOGY_TAGS)
57+
technology?: TechnologyTag;
58+
59+
@Prop({type: Map, of: Number, default: {}})
60+
@IsOptional()
61+
@ApiPropertyOptional({
62+
description: 'Initial cost of resources for the job',
63+
...RESOURCES_SCHEMA_PROPERTIES,
64+
})
65+
cost?: Record<ResourceName, number>;
66+
}
67+
68+
export const JobSchema = SchemaFactory.createForClass(Job);

src/job/job.service.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {Injectable} from "@nestjs/common";
2+
import {InjectModel} from "@nestjs/mongoose";
3+
import {Job} from "./job.schema";
4+
import {Model} from "mongoose";
5+
import {EventRepository, EventService, MongooseRepository} from "@mean-stream/nestx";
6+
7+
@Injectable()
8+
@EventRepository()
9+
export class JobService extends MongooseRepository<Job> {
10+
constructor(
11+
@InjectModel(Job.name) private jobModel: Model<Job>,
12+
private eventEmitter: EventService,
13+
) {
14+
super(jobModel);
15+
}
16+
}

0 commit comments

Comments
 (0)