🏛️ Najważniejsze wyzwanie imperium! Consul Caesar.js powierzył Ci największe zadanie - stworzenie kompletnego systemu zarządzania wszystkimi tributami rzymskiego imperium. To będzie test wszystkich umiejętności z Kontrolerów, Middleware, Guards, Interceptors, Pipes, WebSockets i innych zaawansowanych funkcji NestJS!
System musi obsługiwać:
1// app.module.ts
2import { Module } from '@nestjs/common';
3import { ConfigModule } from '@nestjs/config';
4import { TypeOrmModule } from '@nestjs/typeorm';
5import { BullModule } from '@nestjs/bull';
6import { ScheduleModule } from '@nestjs/schedule';
7import { CacheModule } from '@nestjs/cache-manager';
8import { ThrottlerModule } from '@nestjs/throttler';
9
10// Feature modules
11import { AuthModule } from './auth/auth.module';
12import { TributeModule } from './tribute/tribute.module';
13import { UserModule } from './user/user.module';
14import { WebsocketModule } from './websocket/websocket.module';
15import { AnalyticsModule } from './analytics/analytics.module';
16import { NotificationModule } from './notification/notification.module';
17
18// Configuration
19import { databaseConfig, redisConfig, appConfig } from './config';
20
21@Module({
22 imports: [
23 // Core configuration
24 ConfigModule.forRoot({
25 isGlobal: true,
26 load: [databaseConfig, redisConfig, appConfig],
27 }),
28
29 // Database
30 TypeOrmModule.forRootAsync({
31 useFactory: (configService: ConfigService) => configService.get('database'),
32 inject: [ConfigService],
33 }),
34
35 // Redis for caching and queues
36 BullModule.forRootAsync({
37 useFactory: (configService: ConfigService) => configService.get('redis'),
38 inject: [ConfigService],
39 }),
40
41 CacheModule.registerAsync({
42 useFactory: (configService: ConfigService) => ({
43 store: 'redis',
44 ...configService.get('redis'),
45 ttl: 300,
46 }),
47 inject: [ConfigService],
48 }),
49
50 // Rate limiting
51 ThrottlerModule.forRoot({
52 ttl: 60,
53 limit: 100,
54 }),
55
56 // Scheduling
57 ScheduleModule.forRoot(),
58
59 // Feature modules
60 AuthModule,
61 TributeModule,
62 UserModule,
63 WebsocketModule,
64 AnalyticsModule,
65 NotificationModule,
66 ],
67 providers: [
68 // Global guards
69 {
70 provide: APP_GUARD,
71 useClass: JwtAuthGuard,
72 },
73 {
74 provide: APP_GUARD,
75 useClass: RolesGuard,
76 },
77 {
78 provide: APP_GUARD,
79 useClass: ThrottlerGuard,
80 },
81 // Global interceptors
82 {
83 provide: APP_INTERCEPTOR,
84 useClass: TransformInterceptor,
85 },
86 {
87 provide: APP_INTERCEPTOR,
88 useClass: LoggingInterceptor,
89 },
90 // Global pipes
91 {
92 provide: APP_PIPE,
93 useClass: ValidationPipe,
94 },
95 // Global filters
96 {
97 provide: APP_FILTER,
98 useClass: AllExceptionsFilter,
99 },
100 ],
101})
102export class AppModule {}1// tribute/tribute.entity.ts
2import {
3 Entity,
4 PrimaryGeneratedColumn,
5 Column,
6 CreateDateColumn,
7 UpdateDateColumn,
8 ManyToOne,
9 OneToMany,
10 Index,
11 BeforeInsert,
12 BeforeUpdate,
13} from 'typeorm';
14import { User } from '../user/user.entity';
15import { TributeHistory } from './tribute-history.entity';
16
17export enum TributeCategory {
18 AURUM = 'AURUM', // Gold
19 ARGENTUM = 'ARGENTUM', // Silver
20 GEMMAE = 'GEMMAE', // Gems
21 ARTIFACTA = 'ARTIFACTA', // Artifacts
22 ARMA = 'ARMA', // Weapons
23 MAPPA = 'MAPPA', // Maps
24 MALEDICTUM = 'MALEDICTUM', // Cursed
25}
26
27export enum TributeRarity {
28 COMMON = 'COMMON',
29 UNCOMMON = 'UNCOMMON',
30 RARE = 'RARE',
31 EPIC = 'EPIC',
32 LEGENDARY = 'LEGENDARY',
33 MYTHICAL = 'MYTHICAL',
34}
35
36@Entity('tributes')
37@Index(['category', 'rarity'])
38@Index(['collectedLocation'])
39@Index(['currentValue'])
40@Index(['collectedAt'])
41export class Tribute {
42 @PrimaryGeneratedColumn('uuid')
43 id: string;
44
45 @Column({ length: 255 })
46 name: string;
47
48 @Column('text')
49 description: string;
50
51 @Column({
52 type: 'enum',
53 enum: TributeCategory,
54 })
55 category: TributeCategory;
56
57 @Column({
58 type: 'enum',
59 enum: TributeRarity,
60 })
61 rarity: TributeRarity;
62
63 @Column('decimal', { precision: 15, scale: 2 })
64 currentValue: number;
65
66 @Column('decimal', { precision: 15, scale: 2 })
67 originalValue: number;
68
69 @Column('decimal', { precision: 10, scale: 3, nullable: true })
70 weight: number;
71
72 @Column({ type: 'json' })
73 dimensions: {
74 length: number;
75 width: number;
76 height: number;
77 unit: string;
78 };
79
80 @Column()
81 collectedLocation: string;
82
83 @Column({ type: 'point', nullable: true })
84 coordinates: string;
85
86 @Column({ default: false })
87 isCursed: boolean;
88
89 @Column({ type: 'text', nullable: true })
90 curseDescription: string;
91
92 @Column({ default: false })
93 isAuthenticated: boolean;
94
95 @Column({ type: 'json', nullable: true })
96 authenticationDetails: {
97 authenticatedBy: string;
98 method: string;
99 confidence: number;
100 date: Date;
101 };
102
103 @Column({ type: 'simple-array', nullable: true })
104 imageUrls: string[];
105
106 @Column({ type: 'json', nullable: true })
107 metadata: {
108 historicalPeriod: string;
109 culture: string;
110 estimatedAge: number;
111 condition: string;
112 provenance: string[];
113 previousOwners: string[];
114 };
115
116 @ManyToOne(() => User, user => user.tributes)
117 collectedBy: User;
118
119 @ManyToOne(() => User, user => user.ownedTributes, { nullable: true })
120 currentOwner: User;
121
122 @OneToMany(() => TributeHistory, history => history.tribute)
123 history: TributeHistory[];
124
125 @CreateDateColumn()
126 createdAt: Date;
127
128 @UpdateDateColumn()
129 updatedAt: Date;
130
131 @Column({ type: 'timestamp', nullable: true })
132 collectedAt: Date;
133
134 @Column({ type: 'timestamp', nullable: true })
135 lastValuedAt: Date;
136
137 // Automatic rarity calculation
138 @BeforeInsert()
139 @BeforeUpdate()
140 calculateRarity() {
141 if (this.currentValue < 100) {
142 this.rarity = TributeRarity.COMMON;
143 } else if (this.currentValue < 1000) {
144 this.rarity = TributeRarity.UNCOMMON;
145 } else if (this.currentValue < 10000) {
146 this.rarity = TributeRarity.RARE;
147 } else if (this.currentValue < 100000) {
148 this.rarity = TributeRarity.EPIC;
149 } else if (this.currentValue < 1000000) {
150 this.rarity = TributeRarity.LEGENDARY;
151 } else {
152 this.rarity = TributeRarity.MYTHICAL;
153 }
154 }
155}1// tribute/tribute.service.ts
2import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository, SelectQueryBuilder } from 'typeorm';
5import { InjectQueue } from '@nestjs/bull';
6import { Queue } from 'bull';
7import { EventEmitter2 } from '@nestjs/event-emitter';
8import { Tribute } from './tribute.entity';
9import { CreateTributeDto, UpdateTributeDto, TributeSearchDto } from './dto';
10import { TributeHistoryService } from './tribute-history.service';
11import { ImageProcessingService } from '../shared/image-processing.service';
12
13@Injectable()
14export class TributeService {
15 constructor(
16 @InjectRepository(Tribute)
17 private tributeRepository: Repository<Tribute>,
18 @InjectQueue('tribute-processing') private tributeQueue: Queue,
19 @InjectQueue('image-processing') private imageQueue: Queue,
20 private tributeHistoryService: TributeHistoryService,
21 private imageProcessingService: ImageProcessingService,
22 private eventEmitter: EventEmitter2,
23 ) {}
24
25 async create(createDto: CreateTributeDto, collectedBy: User): Promise<Tribute> {
26 const tribute = this.tributeRepository.create({
27 ...createDto,
28 collectedBy,
29 currentOwner: collectedBy,
30 originalValue: createDto.currentValue,
31 collectedAt: new Date(),
32 });
33
34 const savedTribute = await this.tributeRepository.save(tribute);
35
36 // Background tasks
37 await this.tributeQueue.add('authenticate-tribute', {
38 tributeId: savedTribute.id,
39 });
40
41 if (createDto.imageUrls?.length > 0) {
42 await this.imageQueue.add('process-tribute-images', {
43 tributeId: savedTribute.id,
44 imageUrls: createDto.imageUrls,
45 });
46 }
47
48 // Create history entry
49 await this.tributeHistoryService.createEntry(savedTribute, 'COLLECTED', {
50 collectedBy: collectedBy.id,
51 location: createDto.collectedLocation,
52 });
53
54 // Emit event for real-time notifications
55 this.eventEmitter.emit('tribute.collected', {
56 tribute: savedTribute,
57 collectedBy,
58 });
59
60 return savedTribute;
61 }
62
63 async findAll(searchDto: TributeSearchDto = {}): Promise<{
64 tributes: Tribute[];
65 total: number;
66 page: number;
67 limit: number;
68 }> {
69 const query = this.createSearchQuery(searchDto);
70
71 const page = searchDto.page || 1;
72 const limit = Math.min(searchDto.limit || 20, 100);
73 const offset = (page - 1) * limit;
74
75 const [tributes, total] = await query
76 .skip(offset)
77 .take(limit)
78 .getManyAndCount();
79
80 return {
81 tributes,
82 total,
83 page,
84 limit,
85 };
86 }
87
88 private createSearchQuery(searchDto: TributeSearchDto): SelectQueryBuilder<Tribute> {
89 const query = this.tributeRepository
90 .createQueryBuilder('tribute')
91 .leftJoinAndSelect('tribute.collectedBy', 'collectedBy')
92 .leftJoinAndSelect('tribute.currentOwner', 'currentOwner');
93
94 // Text search
95 if (searchDto.search) {
96 query.andWhere(
97 '(tribute.name ILIKE :search OR tribute.description ILIKE :search)',
98 { search: `%${searchDto.search}%` }
99 );
100 }
101
102 // Category filter
103 if (searchDto.category) {
104 query.andWhere('tribute.category = :category', { category: searchDto.category });
105 }
106
107 // Rarity filter
108 if (searchDto.rarity) {
109 query.andWhere('tribute.rarity = :rarity', { rarity: searchDto.rarity });
110 }
111
112 // Value range
113 if (searchDto.minValue !== undefined) {
114 query.andWhere('tribute.currentValue >= :minValue', { minValue: searchDto.minValue });
115 }
116 if (searchDto.maxValue !== undefined) {
117 query.andWhere('tribute.currentValue <= :maxValue', { maxValue: searchDto.maxValue });
118 }
119
120 // Location search
121 if (searchDto.location) {
122 query.andWhere('tribute.collectedLocation ILIKE :location', {
123 location: `%${searchDto.location}%`
124 });
125 }
126
127 // Date range
128 if (searchDto.collectedAfter) {
129 query.andWhere('tribute.collectedAt >= :collectedAfter', {
130 collectedAfter: searchDto.collectedAfter
131 });
132 }
133 if (searchDto.collectedBefore) {
134 query.andWhere('tribute.collectedAt <= :collectedBefore', {
135 collectedBefore: searchDto.collectedBefore
136 });
137 }
138
139 // Cursed filter
140 if (searchDto.isCursed !== undefined) {
141 query.andWhere('tribute.isCursed = :isCursed', { isCursed: searchDto.isCursed });
142 }
143
144 // Authenticated filter
145 if (searchDto.isAuthenticated !== undefined) {
146 query.andWhere('tribute.isAuthenticated = :isAuthenticated', {
147 isAuthenticated: searchDto.isAuthenticated
148 });
149 }
150
151 // Sorting
152 const sortField = searchDto.sortBy || 'createdAt';
153 const sortOrder = searchDto.sortOrder || 'DESC';
154 query.orderBy(`tribute.${sortField}`, sortOrder);
155
156 return query;
157 }
158
159 async findById(id: string): Promise<Tribute> {
160 const tribute = await this.tributeRepository
161 .createQueryBuilder('tribute')
162 .leftJoinAndSelect('tribute.collectedBy', 'collectedBy')
163 .leftJoinAndSelect('tribute.currentOwner', 'currentOwner')
164 .leftJoinAndSelect('tribute.history', 'history')
165 .where('tribute.id = :id', { id })
166 .getOne();
167
168 if (!tribute) {
169 throw new NotFoundException(`Tribute with ID ${id} not found`);
170 }
171
172 return tribute;
173 }
174
175 async updateValue(id: string, newValue: number, updatedBy: User): Promise<Tribute> {
176 const tribute = await this.findById(id);
177 const oldValue = tribute.currentValue;
178
179 tribute.currentValue = newValue;
180 tribute.lastValuedAt = new Date();
181
182 const updatedTribute = await this.tributeRepository.save(tribute);
183
184 // Create history entry
185 await this.tributeHistoryService.createEntry(tribute, 'VALUE_UPDATED', {
186 oldValue,
187 newValue,
188 updatedBy: updatedBy.id,
189 changePercentage: ((newValue - oldValue) / oldValue) * 100,
190 });
191
192 // Emit event for real-time updates
193 this.eventEmitter.emit('tribute.value.updated', {
194 tribute: updatedTribute,
195 oldValue,
196 newValue,
197 updatedBy,
198 });
199
200 return updatedTribute;
201 }
202
203 async transferOwnership(
204 id: string,
205 newOwnerId: string,
206 transferredBy: User,
207 reason?: string
208 ): Promise<Tribute> {
209 const tribute = await this.findById(id);
210 const oldOwner = tribute.currentOwner;
211
212 // Validation
213 if (tribute.currentOwner?.id !== transferredBy.id) {
214 throw new BadRequestException('Only current owner can transfer tribute');
215 }
216
217 tribute.currentOwner = { id: newOwnerId } as User;
218 const updatedTribute = await this.tributeRepository.save(tribute);
219
220 // Create history entry
221 await this.tributeHistoryService.createEntry(tribute, 'OWNERCOHORT_TRANSFERRED', {
222 fromOwner: oldOwner?.id,
223 toOwner: newOwnerId,
224 transferredBy: transferredBy.id,
225 reason,
226 });
227
228 // Emit event
229 this.eventEmitter.emit('tribute.ownership.transferred', {
230 tribute: updatedTribute,
231 fromOwner: oldOwner,
232 toOwner: newOwnerId,
233 transferredBy,
234 });
235
236 return updatedTribute;
237 }
238
239 async getStatistics(): Promise<any> {
240 const [
241 totalCount,
242 totalValue,
243 categoryStats,
244 rarityStats,
245 recentCollections,
246 ] = await Promise.all([
247 this.tributeRepository.count(),
248 this.tributeRepository
249 .createQueryBuilder('tribute')
250 .select('SUM(tribute.currentValue)', 'total')
251 .getRawOne(),
252 this.getCategoryStatistics(),
253 this.getRarityStatistics(),
254 this.getRecentCollections(),
255 ]);
256
257 return {
258 total: {
259 count: totalCount,
260 value: parseFloat(totalValue.total || '0'),
261 },
262 categories: categoryStats,
263 rarities: rarityStats,
264 recent: recentCollections,
265 };
266 }
267
268 private async getCategoryStatistics() {
269 return await this.tributeRepository
270 .createQueryBuilder('tribute')
271 .select('tribute.category', 'category')
272 .addSelect('COUNT(*)', 'count')
273 .addSelect('SUM(tribute.currentValue)', 'totalValue')
274 .addSelect('AVG(tribute.currentValue)', 'averageValue')
275 .groupBy('tribute.category')
276 .getRawMany();
277 }
278
279 private async getRarityStatistics() {
280 return await this.tributeRepository
281 .createQueryBuilder('tribute')
282 .select('tribute.rarity', 'rarity')
283 .addSelect('COUNT(*)', 'count')
284 .addSelect('SUM(tribute.currentValue)', 'totalValue')
285 .groupBy('tribute.rarity')
286 .orderBy('COUNT(*)', 'DESC')
287 .getRawMany();
288 }
289
290 private async getRecentCollections(limit: number = 10) {
291 return await this.tributeRepository
292 .createQueryBuilder('tribute')
293 .leftJoinAndSelect('tribute.collectedBy', 'collectedBy')
294 .orderBy('tribute.collectedAt', 'DESC')
295 .limit(limit)
296 .getMany();
297 }
298}1// tribute/tribute.controller.ts
2import {
3 Controller,
4 Get,
5 Post,
6 Put,
7 Delete,
8 Body,
9 Param,
10 Query,
11 UseGuards,
12 UseInterceptors,
13 UploadedFiles,
14 Req,
15} from '@nestjs/common';
16import { FilesInterceptor } from '@nestjs/platform-express';
17import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
18import { Throttle } from '@nestjs/throttler';
19import { TributeService } from './tribute.service';
20import { CurrentUser } from '../auth/decorators/current-user.decorator';
21import { Roles } from '../auth/decorators/roles.decorator';
22import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
23import { RolesGuard } from '../auth/guards/roles.guard';
24import { CacheInterceptor } from '../shared/interceptors/cache.interceptor';
25import { LoggingInterceptor } from '../shared/interceptors/logging.interceptor';
26import { TransformInterceptor } from '../shared/interceptors/transform.interceptor';
27import { CreateTributeDto, UpdateTributeDto, TributeSearchDto } from './dto';
28import { User } from '../user/user.entity';
29
30@ApiTags('tributes')
31@ApiBearerAuth()
32@Controller('tributes')
33@UseGuards(JwtAuthGuard, RolesGuard)
34@UseInterceptors(LoggingInterceptor, TransformInterceptor)
35export class TributesController {
36 constructor(private readonly tributeService: TributeService) {}
37
38 @Post()
39 @Roles('MILES', 'CENTURION', 'TRIBUNE')
40 @UseInterceptors(FilesInterceptor('images', 5))
41 @ApiOperation({ summary: 'Collect a new tribute' })
42 @ApiResponse({ status: 201, description: 'Tribute collected successfully' })
43 async create(
44 @Body() createDto: CreateTributeDto,
45 @CurrentUser() user: User,
46 @UploadedFiles() images: Express.Multer.File[],
47 ) {
48 if (images?.length > 0) {
49 createDto.imageUrls = await this.uploadImages(images);
50 }
51
52 return await this.tributeService.create(createDto, user);
53 }
54
55 @Get()
56 @UseInterceptors(CacheInterceptor)
57 @Throttle(60, 60) // 60 requests per minute
58 @ApiOperation({ summary: 'Search tributes' })
59 async findAll(@Query() searchDto: TributeSearchDto) {
60 return await this.tributeService.findAll(searchDto);
61 }
62
63 @Get('statistics')
64 @Roles('CENTURION', 'TRIBUNE')
65 @UseInterceptors(CacheInterceptor)
66 @ApiOperation({ summary: 'Get tribute statistics' })
67 async getStatistics() {
68 return await this.tributeService.getStatistics();
69 }
70
71 @Get('my-tributes')
72 @ApiOperation({ summary: 'Get current user tributes' })
73 async getMyTributes(
74 @CurrentUser() user: User,
75 @Query() searchDto: TributeSearchDto,
76 ) {
77 return await this.tributeService.findUserTributes(user.id, searchDto);
78 }
79
80 @Get(':id')
81 @ApiOperation({ summary: 'Get tribute by ID' })
82 async findOne(@Param('id') id: string) {
83 return await this.tributeService.findById(id);
84 }
85
86 @Put(':id')
87 @Roles('MILES', 'CENTURION', 'TRIBUNE')
88 @ApiOperation({ summary: 'Update tribute' })
89 async update(
90 @Param('id') id: string,
91 @Body() updateDto: UpdateTributeDto,
92 @CurrentUser() user: User,
93 ) {
94 return await this.tributeService.update(id, updateDto, user);
95 }
96
97 @Put(':id/value')
98 @Roles('APPRAISER', 'CENTURION', 'TRIBUNE')
99 @ApiOperation({ summary: 'Update tribute value' })
100 async updateValue(
101 @Param('id') id: string,
102 @Body('value') value: number,
103 @CurrentUser() user: User,
104 ) {
105 return await this.tributeService.updateValue(id, value, user);
106 }
107
108 @Put(':id/transfer')
109 @Roles('MILES', 'CENTURION', 'TRIBUNE')
110 @ApiOperation({ summary: 'Transfer tribute ownership' })
111 async transferOwnership(
112 @Param('id') id: string,
113 @Body() transferDto: { newOwnerId: string; reason?: string },
114 @CurrentUser() user: User,
115 ) {
116 return await this.tributeService.transferOwnership(
117 id,
118 transferDto.newOwnerId,
119 user,
120 transferDto.reason,
121 );
122 }
123
124 @Put(':id/authenticate')
125 @Roles('APPRAISER', 'EXPERT', 'TRIBUNE')
126 @ApiOperation({ summary: 'Authenticate tribute' })
127 async authenticate(
128 @Param('id') id: string,
129 @Body() authDto: any,
130 @CurrentUser() user: User,
131 ) {
132 return await this.tributeService.authenticate(id, authDto, user);
133 }
134
135 @Delete(':id')
136 @Roles('CENTURION', 'TRIBUNE')
137 @ApiOperation({ summary: 'Remove tribute (mark as lost)' })
138 async remove(
139 @Param('id') id: string,
140 @CurrentUser() user: User,
141 @Body('reason') reason?: string,
142 ) {
143 return await this.tributeService.markAsLost(id, user, reason);
144 }
145
146 private async uploadImages(images: Express.Multer.File[]): Promise<string[]> {
147 // Implementation for image upload to cloud storage
148 // Return array of uploaded image URLs
149 return [];
150 }
151}1// Core CRUD
2POST /api/tributes # Create tribute
3GET /api/tributes # Search tributes
4GET /api/tributes/:id # Get tribute details
5PUT /api/tributes/:id # Update tribute
6DELETE /api/tributes/:id # Mark as lost
7
8// Specialized operations
9PUT /api/tributes/:id/value # Update value
10PUT /api/tributes/:id/transfer # Transfer ownership
11PUT /api/tributes/:id/authenticate # Authenticate tribute
12POST /api/tributes/:id/images # Upload images
13
14// Analytics
15GET /api/tributes/statistics # Overall statistics
16GET /api/tributes/trends # Value trends
17GET /api/tributes/my-tributes # User's tributes
18
19// Admin operations
20GET /api/tributes/audit # Audit logs
21POST /api/tributes/bulk-import # Bulk import
22POST /api/tributes/export # Export data
23
24// WebSocket events
25tribute:collected # New tribute collected
26tribute:updated # Tribute modified
27tribute:transferred # Ownership changed
28value:updated # Price changed
29alert:suspicious # Suspicious activityTo ultimatywne wyzwanie dla prawdziwego rzymskiego programisty! Powodzenia w tworzeniu najlepszego systemu zarządzania tributami w całym imperium!