From 8c44a8950f30b12fdaf5d22de25898ff8430a9a6 Mon Sep 17 00:00:00 2001 From: Victor Peter Date: Wed, 17 Jun 2026 22:28:40 +0100 Subject: [PATCH 1/3] feat(backend): ranked event leaderboard replacing binary winners model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [Backend] — Ranked event leaderboard (replaces binary "winners" model) ## Problem The platform had no points-based leaderboard for creator events. The only related concept was a binary "winners" list returned by `getEventWinners()` on the contract, surfaced through the abandoned `winners-query.dto.ts` / `WinnerResponse` model. There was no ranked, paginated, accuracy-based view of participants. ## What Was Done ### 1. ContractService — new interface + method File: `src/contract/contract.service.ts` Added the `ContractLeaderboardEntry` interface (rank, address, total_predictions, correct_predictions, accuracy_percentage, is_winner, completion_time) alongside the existing `ContractWinner` interface. Added `getEventLeaderboard(eventId)` which calls the new `get_event_leaderboard` Soroban view function, normalises every field to the correct primitive type, and falls back to an empty array on contract error — matching the defensive pattern used by all other view calls in this service. ### 2. New entity File: `src/matches/entities/creator-event-leaderboard-entry.entity.ts` TypeORM entity `CreatorEventLeaderboardEntry` mapped to the table `creator_event_leaderboard_entries`. Columns mirror `ContractLeaderboardEntry` plus a UUID primary key and `created_at` timestamp. Unique index on (event_id, user_address) prevents duplicate entries; composite index on (event_id, rank) optimises the ORDER BY rank queries used by `getLeaderboard()`. ### 3. Migration File: `src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts` Creates the `creator_event_leaderboard_entries` table with all columns and both indexes in `up()`. `down()` drops the table entirely. Timestamp 1750200000000 places this after all existing migrations. ### 4. Rewritten leaderboard-query.dto.ts File: `src/creator-events/dto/leaderboard-query.dto.ts` Replaced the old stub (which had a dangling `minPredictions` field and an incomplete `LeaderboardEntryResponse` lacking `totalPages` and `source`) with a clean DTO. `LeaderboardQueryDto` keeps `page` / `limit` with class-validator decorators. `PaginatedLeaderboardResponse` now includes `totalPages` and a `source` discriminator ("contract" | "cache") so callers know where the data came from. ### 5. getLeaderboard() service method File: `src/creator-events/creator-events.service.ts` New `getLeaderboard(eventId, query)` method with dual-path logic: - **Finalized path**: looks up the `CreatorEvent` row by `on_chain_event_id` and checks `is_finalized`. If true, queries `CreatorEventLeaderboardEntry` rows ordered by rank with TypeORM `findAndCount` for efficient pagination, and returns `source: "cache"`. - **Live path**: calls `contractService.getEventLeaderboard(eventId)`, paginates the result array in-process (the contract returns the full list), and returns `source: "contract"`. The `CreatorEventLeaderboardEntry` repository was injected alongside the existing `CreatorEvent` repository via `@InjectRepository`. Imports for `LeaderboardQueryDto`, `LeaderboardEntryResponse`, and `PaginatedLeaderboardResponse` were added. ### 6. New endpoint File: `src/creator-events/creator-events.controller.ts` `GET /creator-events/:id/leaderboard` added to `CreatorEventsController`. Uses `CacheInterceptor` with a 30-second TTL, a `ValidationPipe` with transform/whitelist on the query, and Swagger annotations (`@ApiOperation`, `@ApiQuery`, `@ApiResponse`). ### 7. Module update File: `src/creator-events/creator-events.module.ts` `CreatorEventLeaderboardEntry` added to `TypeOrmModule.forFeature`. Added `exports: [CreatorEventsService]` for future cross-module use. ### 8. Deleted unused files - `src/creator-events/dto/winners-query.dto.ts` — removed entirely. `WinnersQueryDto`, `WinnerResponse`, and `PaginatedWinnersResponse` are no longer referenced anywhere in the codebase. ## How It Works End-to-End 1. A creator event is active → `GET /creator-events/:id/leaderboard` calls `contractService.getEventLeaderboard()` (live Soroban view), paginates the result, returns `source: "contract"`. 2. The indexer's `EventFinalized` handler (Issue 6) sets `is_finalized = true` on the `CreatorEvent` row and populates `creator_event_leaderboard_entries` from the final contract state. 3. Subsequent calls to the same endpoint detect `is_finalized = true` and read from the DB cache instead, returning `source: "cache"`. This avoids repeated Soroban RPC calls for settled events and enables fast, indexed pagination at scale. ## Acceptance Criteria Met - GET /creator-events/:id/leaderboard?page=1&limit=20 returns a paginated ranked response. - Non-finalized event → live contract read. - Finalized event → DB cache read. - winners-query.dto.ts and WinnerResponse/ContractWinner models are removed / superseded. --- backend/src/contract/contract.service.ts | 29 ++++++++ .../creator-events.controller.ts | 22 ++++++ .../creator-events/creator-events.module.ts | 4 +- .../creator-events/creator-events.service.ts | 74 +++++++++++++++++++ .../dto/leaderboard-query.dto.ts | 16 +--- .../creator-events/dto/winners-query.dto.ts | 40 ---------- .../creator-event-leaderboard-entry.entity.ts | 54 ++++++++++++++ ...0000-CreateCreatorEventLeaderboardEntry.ts | 65 ++++++++++++++++ 8 files changed, 251 insertions(+), 53 deletions(-) delete mode 100644 backend/src/creator-events/dto/winners-query.dto.ts create mode 100644 backend/src/matches/entities/creator-event-leaderboard-entry.entity.ts create mode 100644 backend/src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts diff --git a/backend/src/contract/contract.service.ts b/backend/src/contract/contract.service.ts index 9fd03dfb..562affe8 100644 --- a/backend/src/contract/contract.service.ts +++ b/backend/src/contract/contract.service.ts @@ -85,6 +85,16 @@ export interface ContractWinner { payout: string; } +export interface ContractLeaderboardEntry { + rank: number; + address: string; + total_predictions: number; + correct_predictions: number; + accuracy_percentage: number; + is_winner: boolean; + completion_time: string | null; +} + export interface ContractConfig { admin: string; aiAgent: string; @@ -183,6 +193,25 @@ export class ContractService { return result ?? []; } + async getEventLeaderboard( + eventId: string, + ): Promise { + const result = await this.viewCall( + 'get_event_leaderboard', + [nativeToScVal(eventId, { type: 'string' })], + ); + if (!result || !Array.isArray(result)) return []; + return result.map((entry, i) => ({ + rank: entry.rank ?? i + 1, + address: entry.address, + total_predictions: Number(entry.total_predictions ?? 0), + correct_predictions: Number(entry.correct_predictions ?? 0), + accuracy_percentage: Number(entry.accuracy_percentage ?? 0), + is_winner: Boolean(entry.is_winner ?? false), + completion_time: entry.completion_time ?? null, + })); + } + async getConfig(): Promise { return this.viewCall('get_config', []); } diff --git a/backend/src/creator-events/creator-events.controller.ts b/backend/src/creator-events/creator-events.controller.ts index 6863b0c0..e1ec3fa5 100644 --- a/backend/src/creator-events/creator-events.controller.ts +++ b/backend/src/creator-events/creator-events.controller.ts @@ -24,6 +24,7 @@ import { CreatorEventsService } from './creator-events.service'; import { EventByCodeResponseDto } from './dto/event-by-code-response.dto'; import { ListMatchesQueryDto } from './dto/list-matches-query.dto'; import { ListParticipantsQueryDto } from './dto/list-participants-query.dto'; +import { LeaderboardQueryDto } from './dto/leaderboard-query.dto'; import { SearchEventsQueryDto } from './dto/search-events-query.dto'; import { SearchEventsResponseDto } from './dto/search-events-response.dto'; import { UserScoreResponseDto } from './dto/user-score-response.dto'; @@ -101,6 +102,27 @@ export class CreatorEventsController { return this.creatorEventsService.getParticipants(id, query); } + /** + * GET /api/creator-events/:id/leaderboard + * Ranked event leaderboard. Reads from DB cache for finalized events, + * falls back to live contract view otherwise. + */ + @Get(':id/leaderboard') + @UseInterceptors(CacheInterceptor) + @CacheTTL(30) // 30 seconds + @ApiOperation({ summary: 'Get ranked leaderboard for an event' }) + @ApiQuery({ name: 'page', required: false, example: 1 }) + @ApiQuery({ name: 'limit', required: false, example: 20 }) + @ApiResponse({ status: 200, description: 'Paginated leaderboard entries' }) + @ApiResponse({ status: 404, description: 'Event not found' }) + getLeaderboard( + @Param('id') id: string, + @Query(new ValidationPipe({ transform: true, whitelist: true })) + query: LeaderboardQueryDto, + ) { + return this.creatorEventsService.getLeaderboard(id, query); + } + /** * GET /api/creator-events/:id/matches * #728 — Fetch all matches for an event with filtering and sorting. diff --git a/backend/src/creator-events/creator-events.module.ts b/backend/src/creator-events/creator-events.module.ts index 021ea60b..3ae04240 100644 --- a/backend/src/creator-events/creator-events.module.ts +++ b/backend/src/creator-events/creator-events.module.ts @@ -3,6 +3,7 @@ import { CacheModule } from '@nestjs/cache-manager'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ContractModule } from '../contract/contract.module'; import { CreatorEvent } from '../matches/entities/creator-event.entity'; +import { CreatorEventLeaderboardEntry } from '../matches/entities/creator-event-leaderboard-entry.entity'; import { AdminCreatorEventsController, CreatorEventsController, @@ -13,7 +14,7 @@ import { CreatorEventsService } from './creator-events.service'; @Module({ imports: [ ContractModule, - TypeOrmModule.forFeature([CreatorEvent]), + TypeOrmModule.forFeature([CreatorEvent, CreatorEventLeaderboardEntry]), CacheModule.register(), ], controllers: [ @@ -22,5 +23,6 @@ import { CreatorEventsService } from './creator-events.service'; AdminCreatorEventsController, ], providers: [CreatorEventsService], + exports: [CreatorEventsService], }) export class CreatorEventsModule {} diff --git a/backend/src/creator-events/creator-events.service.ts b/backend/src/creator-events/creator-events.service.ts index dac02f46..bca46b2d 100644 --- a/backend/src/creator-events/creator-events.service.ts +++ b/backend/src/creator-events/creator-events.service.ts @@ -9,6 +9,7 @@ import { ContractMatch, } from '../contract/contract.service'; import { CreatorEvent } from '../matches/entities/creator-event.entity'; +import { CreatorEventLeaderboardEntry } from '../matches/entities/creator-event-leaderboard-entry.entity'; import { EventByCodeResponseDto, MatchPreviewDto, @@ -36,6 +37,11 @@ import { import { UserScoreResponseDto } from './dto/user-score-response.dto'; import { UserPredictionsResponseDto } from './dto/user-predictions-response.dto'; import { EventStatsResponseDto } from './dto/event-stats-response.dto'; +import { + LeaderboardQueryDto, + LeaderboardEntryResponse, + PaginatedLeaderboardResponse, +} from './dto/leaderboard-query.dto'; import { normalizeContractPrediction, resolveCorrectness, @@ -74,6 +80,8 @@ export class CreatorEventsService { private readonly contractService: ContractService, @InjectRepository(CreatorEvent) private readonly creatorEventRepository: Repository, + @InjectRepository(CreatorEventLeaderboardEntry) + private readonly leaderboardEntryRepository: Repository, ) {} async searchEvents( @@ -688,4 +696,70 @@ export class CreatorEventsService { private escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + + async getLeaderboard( + eventId: string, + query: LeaderboardQueryDto, + ): Promise { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + + // Check if event is finalized via DB cache + const cachedEvent = await this.creatorEventRepository.findOne({ + where: { on_chain_event_id: Number(eventId) as unknown as number }, + select: ['is_finalized'], + }); + + if (cachedEvent?.is_finalized) { + const [entries, total] = await this.leaderboardEntryRepository.findAndCount({ + where: { event_id: eventId }, + order: { rank: 'ASC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data: entries.map((e) => ({ + rank: e.rank, + user_address: e.user_address, + total_predictions: e.total_predictions, + correct_predictions: e.correct_predictions, + accuracy_percentage: Number(e.accuracy_percentage), + is_winner: e.is_winner, + completion_time: e.completion_time + ? e.completion_time.toISOString() + : null, + })), + total, + page, + limit, + totalPages: Math.ceil(total / limit), + source: 'cache', + }; + } + + // Live read from contract + const all = await this.contractService.getEventLeaderboard(eventId); + const total = all.length; + const slice = all.slice((page - 1) * limit, page * limit); + + const data: LeaderboardEntryResponse[] = slice.map((e) => ({ + rank: e.rank, + user_address: e.address, + total_predictions: e.total_predictions, + correct_predictions: e.correct_predictions, + accuracy_percentage: e.accuracy_percentage, + is_winner: e.is_winner, + completion_time: e.completion_time ?? null, + })); + + return { + data, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + source: 'contract', + }; + } } diff --git a/backend/src/creator-events/dto/leaderboard-query.dto.ts b/backend/src/creator-events/dto/leaderboard-query.dto.ts index 5b1ea91f..c6c7a9de 100644 --- a/backend/src/creator-events/dto/leaderboard-query.dto.ts +++ b/backend/src/creator-events/dto/leaderboard-query.dto.ts @@ -8,7 +8,7 @@ export class LeaderboardQueryDto { @Type(() => Number) @IsInt() @Min(1) - page?: number = 1; + page: number = 1; @ApiPropertyOptional({ description: 'Results per page (max 100)', @@ -20,17 +20,7 @@ export class LeaderboardQueryDto { @IsInt() @Min(1) @Max(100) - limit?: number = 20; - - @ApiPropertyOptional({ - description: 'Minimum number of predictions to be included', - default: 1, - }) - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(0) - minPredictions?: number = 0; + limit: number = 20; } export interface LeaderboardEntryResponse { @@ -48,4 +38,6 @@ export interface PaginatedLeaderboardResponse { total: number; page: number; limit: number; + totalPages: number; + source: 'contract' | 'cache'; } diff --git a/backend/src/creator-events/dto/winners-query.dto.ts b/backend/src/creator-events/dto/winners-query.dto.ts deleted file mode 100644 index b81403a8..00000000 --- a/backend/src/creator-events/dto/winners-query.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IsOptional, IsInt, Min, Max } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class WinnersQueryDto { - @ApiPropertyOptional({ description: 'Page number', default: 1, minimum: 1 }) - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - page?: number = 1; - - @ApiPropertyOptional({ - description: 'Results per page (max 100)', - default: 20, - maximum: 100, - }) - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - @Max(100) - limit?: number = 20; -} - -export interface WinnerResponse { - rank: number; - user_address: string; - total_correct: number; - total_matches: number; - completion_time: string; - verified_at: string; -} - -export interface PaginatedWinnersResponse { - data: WinnerResponse[]; - total: number; - page: number; - limit: number; -} diff --git a/backend/src/matches/entities/creator-event-leaderboard-entry.entity.ts b/backend/src/matches/entities/creator-event-leaderboard-entry.entity.ts new file mode 100644 index 00000000..84adddc2 --- /dev/null +++ b/backend/src/matches/entities/creator-event-leaderboard-entry.entity.ts @@ -0,0 +1,54 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; + +@Entity('creator_event_leaderboard_entries') +@Index(['event_id', 'user_address'], { unique: true }) +@Index(['event_id', 'rank']) +export class CreatorEventLeaderboardEntry { + @PrimaryGeneratedColumn('uuid') + @ApiProperty() + id: string; + + @Column({ type: 'varchar', length: 255 }) + @Index() + @ApiProperty() + event_id: string; + + @Column({ type: 'varchar', length: 255 }) + @ApiProperty() + user_address: string; + + @Column({ type: 'int' }) + @ApiProperty() + rank: number; + + @Column({ type: 'int', default: 0 }) + @ApiProperty() + total_predictions: number; + + @Column({ type: 'int', default: 0 }) + @ApiProperty() + correct_predictions: number; + + @Column({ type: 'numeric', precision: 5, scale: 2, default: 0 }) + @ApiProperty() + accuracy_percentage: number; + + @Column({ type: 'boolean', default: false }) + @ApiProperty() + is_winner: boolean; + + @Column({ type: 'timestamptz', nullable: true }) + @ApiProperty({ nullable: true }) + completion_time: Date | null; + + @CreateDateColumn() + @ApiProperty() + created_at: Date; +} diff --git a/backend/src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts b/backend/src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts new file mode 100644 index 00000000..28396c37 --- /dev/null +++ b/backend/src/migrations/1750200000000-CreateCreatorEventLeaderboardEntry.ts @@ -0,0 +1,65 @@ +import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; + +export class CreateCreatorEventLeaderboardEntry1750200000000 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'creator_event_leaderboard_entries', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'uuid_generate_v4()', + }, + { name: 'event_id', type: 'varchar', length: '255', isNullable: false }, + { name: 'user_address', type: 'varchar', length: '255', isNullable: false }, + { name: 'rank', type: 'int', isNullable: false }, + { name: 'total_predictions', type: 'int', default: 0, isNullable: false }, + { name: 'correct_predictions', type: 'int', default: 0, isNullable: false }, + { + name: 'accuracy_percentage', + type: 'numeric', + precision: 5, + scale: 2, + default: 0, + isNullable: false, + }, + { name: 'is_winner', type: 'boolean', default: false, isNullable: false }, + { name: 'completion_time', type: 'timestamptz', isNullable: true }, + { + name: 'created_at', + type: 'timestamptz', + default: 'CURRENT_TIMESTAMP', + isNullable: false, + }, + ], + }), + true, + ); + + await queryRunner.createIndex( + 'creator_event_leaderboard_entries', + new TableIndex({ + name: 'UQ_cel_entry_event_address', + columnNames: ['event_id', 'user_address'], + isUnique: true, + }), + ); + + await queryRunner.createIndex( + 'creator_event_leaderboard_entries', + new TableIndex({ + name: 'IDX_cel_entry_event_rank', + columnNames: ['event_id', 'rank'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('creator_event_leaderboard_entries'); + } +} From b14602c487a848a497e064cc26183eb7a91340e2 Mon Sep 17 00:00:00 2001 From: Victor Peter Date: Wed, 17 Jun 2026 22:36:08 +0100 Subject: [PATCH 2/3] fix tests --- backend/package-lock.json | 265 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/backend/package-lock.json b/backend/package-lock.json index f707b31b..79bf880d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,11 +17,13 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-socket.io": "^11.1.24", "@nestjs/schedule": "^6.1.1", "@nestjs/swagger": "^11.2.6", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.1.24", "@stellar/stellar-sdk": "^14.6.1", "bcrypt": "^6.0.0", "cache-manager": "^7.2.8", @@ -34,6 +36,7 @@ "pino-http": "^11.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "socket.io": "^4.8.3", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.28" }, @@ -2475,6 +2478,25 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.27.tgz", + "integrity": "sha512-xgpLzaIDGOCC6xOAtHnRAz8sqieFgGxxu3MN5ID026Jt6oeL3efp29N5QHhPr7UlqBfy/Jd02uj0POkZq6Au3Q==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.3", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schedule": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.1.tgz", @@ -2761,6 +2783,29 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.27.tgz", + "integrity": "sha512-X3OgJt9KgYTvt9D7sNz9SOj3A1daAHy7DZrYhM1pky8Fh+erlKQH5IQ/tKm+GaJKA5M0srBUr1CMqjak/qNxOw==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@noble/curves": { "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", @@ -2877,6 +2922,12 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", @@ -3083,6 +3134,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3331,6 +3391,15 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4524,6 +4593,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.10", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", @@ -5658,6 +5736,79 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -8771,6 +8922,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10038,6 +10198,90 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -11746,6 +11990,27 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", From 7699b57b491710ec08ac138f9352c3019b064c66 Mon Sep 17 00:00:00 2001 From: Victor Peter Date: Wed, 17 Jun 2026 22:45:56 +0100 Subject: [PATCH 3/3] fix(tests): add CreatorEventLeaderboardEntry repo mock to creator-events specs The new leaderboard repository injected in CreatorEventsService caused both creator-events spec files to fail at module compile time with: Nest can't resolve dependencies of the CreatorEventsService (..., ?). CreatorEventLeaderboardEntryRepository at index [2] Fix: import CreatorEventLeaderboardEntry and provide an empty mock for getRepositoryToken(CreatorEventLeaderboardEntry) in both: - creator-events.service.spec.ts - creator-events-predictions-stats.spec.ts --- .../creator-events/creator-events-predictions-stats.spec.ts | 5 +++++ backend/src/creator-events/creator-events.service.spec.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/backend/src/creator-events/creator-events-predictions-stats.spec.ts b/backend/src/creator-events/creator-events-predictions-stats.spec.ts index b3639565..8586dc5f 100644 --- a/backend/src/creator-events/creator-events-predictions-stats.spec.ts +++ b/backend/src/creator-events/creator-events-predictions-stats.spec.ts @@ -6,6 +6,7 @@ import { ContractService, } from '../contract/contract.service'; import { CreatorEvent } from '../matches/entities/creator-event.entity'; +import { CreatorEventLeaderboardEntry } from '../matches/entities/creator-event-leaderboard-entry.entity'; import { CreatorEventsService } from './creator-events.service'; describe('CreatorEventsService predictions and stats', () => { @@ -74,6 +75,10 @@ describe('CreatorEventsService predictions and stats', () => { provide: getRepositoryToken(CreatorEvent), useValue: { createQueryBuilder: jest.fn() }, }, + { + provide: getRepositoryToken(CreatorEventLeaderboardEntry), + useValue: {}, + }, ], }).compile(); diff --git a/backend/src/creator-events/creator-events.service.spec.ts b/backend/src/creator-events/creator-events.service.spec.ts index bf76778d..82dc7a12 100644 --- a/backend/src/creator-events/creator-events.service.spec.ts +++ b/backend/src/creator-events/creator-events.service.spec.ts @@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository, SelectQueryBuilder } from 'typeorm'; import { ContractService } from '../contract/contract.service'; import { CreatorEvent } from '../matches/entities/creator-event.entity'; +import { CreatorEventLeaderboardEntry } from '../matches/entities/creator-event-leaderboard-entry.entity'; import { CreatorEventsService } from './creator-events.service'; import { CreatorEventSearchStatus } from './dto/search-events-query.dto'; @@ -89,6 +90,10 @@ describe('CreatorEventsService searchEvents', () => { provide: getRepositoryToken(CreatorEvent), useValue: creatorEventRepository, }, + { + provide: getRepositoryToken(CreatorEventLeaderboardEntry), + useValue: {}, + }, ], }).compile();