Skip to content

Commit a809124

Browse files
authored
Merge pull request #193 from game-node-app/dev
IGDB sync issues
2 parents f89a070 + 5775cb6 commit a809124

14 files changed

+148
-23
lines changed

server_swagger.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/achievements/data/achievements-events.data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const achievementsEventsData: Achievement[] = [
66
{
77
id: "awards_2025",
88
name: "Awards 2025",
9-
description: "Participate in the 2025 Game Awards event",
9+
description: "Participate in the 2025 GameNode Awards event",
1010
expGainAmount: 1000,
1111
category: AchievementCategory.EVENTS,
1212
checkEligibility: async (dataSource, targetUserId) => {

src/game/external-game/external-game.service.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ export class ExternalGameService {
5959
where: {
6060
gameId: In(gameIds),
6161
},
62-
cache: {
63-
id: `external-games-ids-${gameIds}`,
64-
milliseconds: days(1),
65-
},
6662
relations: this.relations,
6763
});
6864
}

src/game/game-repository/game-repository-create.service.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { GameTheme } from "./entities/game-theme.entity";
2020
import { GamePlayerPerspective } from "./entities/game-player-perspective.entity";
2121
import { GameEngine } from "./entities/game-engine.entity";
2222
import { GameEngineLogo } from "./entities/game-engine-logo.entity";
23-
import { PartialGame } from "./game-repository.types";
23+
import { IDGBPartialGame } from "./game-repository.types";
2424
import { StatisticsQueueService } from "../../statistics/statistics-queue/statistics-queue.service";
2525
import { StatisticsSourceType } from "../../statistics/statistics.constants";
2626
import { ExternalGameService } from "../external-game/external-game.service";
@@ -100,7 +100,7 @@ export class GameRepositoryCreateService {
100100
private readonly externalGameService: ExternalGameService,
101101
) {}
102102

103-
async shouldUpdate(game: PartialGame) {
103+
async shouldUpdate(game: IDGBPartialGame) {
104104
if (game.id == null || typeof game.id !== "number") {
105105
return false;
106106
} else if (
@@ -121,7 +121,7 @@ export class GameRepositoryCreateService {
121121
* Bug info: https://github.com/typeorm/typeorm/issues/1754
122122
* @param game
123123
*/
124-
async createOrUpdate(game: PartialGame) {
124+
async createOrUpdate(game: IDGBPartialGame) {
125125
const shouldProcess = await this.shouldUpdate(game);
126126

127127
if (!shouldProcess) {
@@ -141,7 +141,7 @@ export class GameRepositoryCreateService {
141141
}
142142

143143
private dispatchCreateUpdateEvent(
144-
game: PartialGame,
144+
game: IDGBPartialGame,
145145
isUpdateAction: boolean,
146146
) {
147147
if (!isUpdateAction) {
@@ -158,7 +158,7 @@ export class GameRepositoryCreateService {
158158
* e.g. Relationships where Game is on the ManyToOne side. Do not update ManyToMany models here.
159159
* @param game
160160
*/
161-
async buildParentRelationships(game: PartialGame) {
161+
async buildParentRelationships(game: IDGBPartialGame) {
162162
if (game.collection) {
163163
// collection.games changes are not cascaded.
164164
const collection = this.gameCollectionRepository.create(
@@ -175,7 +175,7 @@ export class GameRepositoryCreateService {
175175
* <strong> We assume the game is already persisted at this point, so we can use it as a parent.</strong>
176176
* @param game
177177
*/
178-
async buildChildRelationships(game: PartialGame) {
178+
async buildChildRelationships(game: IDGBPartialGame) {
179179
if (game.alternativeNames) {
180180
const alternativeNames = game.alternativeNames.map(
181181
(alternativeName) => {
@@ -231,10 +231,17 @@ export class GameRepositoryCreateService {
231231
*/
232232
if (game.externalGames) {
233233
for (const externalGame of game.externalGames) {
234+
const category =
235+
externalGame.category ?? externalGame.externalGameSource;
236+
const media =
237+
externalGame.media ?? externalGame.gameReleaseFormat;
238+
234239
try {
235240
await this.externalGameService.upsert({
236241
...externalGame,
237242
gameId: game.id,
243+
category,
244+
media,
238245
});
239246
} catch (e) {}
240247
}

src/game/game-repository/game-repository.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class GameRepositoryService {
139139
return games;
140140
}
141141

142-
@Cacheable(GameRepositoryService.name, minutes(30))
142+
// @Cacheable(GameRepositoryService.name, minutes(30))
143143
async findGameExternalStores(gameId: number) {
144144
const externalGames = await this.externalGameService.findAllForGameId([
145145
gameId,
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
import { DeepPartial } from "typeorm";
21
import { Game } from "./entities/game.entity";
2+
import { GameExternalGame } from "../external-game/entity/game-external-game.entity";
3+
import { DeepPartial } from "typeorm";
4+
5+
interface IGDBExternalGame extends GameExternalGame {
6+
// Substitute for deprecated field GameExternalGame#category
7+
externalGameSource?: number;
8+
// Substitute for deprecated field GameExternalGame#media
9+
gameReleaseFormat?: number;
10+
}
311

4-
export type PartialGame = DeepPartial<Game> & {
12+
/**
13+
* Items are received as pascal_case from IGDB API, and then converted to camelCase.
14+
* Assume a camelCase option is available if `IgdbSyncProcessor#normalizeIgdbResults` has run.
15+
*/
16+
export type IDGBPartialGame = DeepPartial<Game> & {
517
id: number;
18+
externalGames: IGDBExternalGame[];
619
[key: string]: any;
720
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class JournalHeatmapItem {
2+
/**
3+
* Date normalized to the 'day' level. In the 'YYYY-MM-DD' format.
4+
* @see https://mantine.dev/charts/heatmap/#data-format
5+
*/
6+
date: string;
7+
count: number;
8+
}
9+
10+
export class GetJournalHeatmapResponseDto {
11+
items: JournalHeatmapItem[];
12+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Injectable } from "@nestjs/common";
2+
import {
3+
GetJournalHeatmapResponseDto,
4+
JournalHeatmapItem,
5+
} from "./dto/get-journal-heatmap.dto";
6+
import dayjs from "dayjs";
7+
import { JournalService } from "./journal.service";
8+
9+
@Injectable()
10+
export class JournalHeatmapService {
11+
constructor(private readonly journalService: JournalService) {}
12+
13+
/**
14+
* Build a heatmap from Journal entry items.
15+
* @param userId
16+
* @param targetUserId
17+
*/
18+
public async buildHeatmap(
19+
userId: string | undefined,
20+
targetUserId: string,
21+
): Promise<GetJournalHeatmapResponseDto> {
22+
const journalOverview = await this.journalService.getOverview(
23+
userId,
24+
targetUserId,
25+
);
26+
27+
const heatmapItems: JournalHeatmapItem[] = [];
28+
29+
/**
30+
* The good code is the friends we made along the way
31+
*/
32+
for (const yearGroup of journalOverview.years) {
33+
const year = yearGroup.year;
34+
for (const monthGroup of yearGroup.months) {
35+
const month = monthGroup.month;
36+
for (const dayGroup of monthGroup.days) {
37+
const day = dayGroup.day;
38+
/**
39+
* @see https://mantine.dev/charts/heatmap/#data-format
40+
*/
41+
const formattedDate = dayjs(
42+
`${year}-${month}-${day}`,
43+
// Automatically prefixes the fields with 0 when necessary (e.g. MM -> 08)
44+
).format("YYYY-MM-DD");
45+
heatmapItems.push({
46+
date: formattedDate,
47+
count: dayGroup.entries.length,
48+
});
49+
}
50+
}
51+
}
52+
53+
return {
54+
items: heatmapItems,
55+
};
56+
}
57+
}

src/journal/journal.constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ export enum JournalPlaylogItemType {
22
COLLECTION_ENTRY_STATUS = "collection_entry_status",
33
OBTAINED_ACHIEVEMENT = "obtained_achievement",
44
}
5+
6+
export enum JournalHeatmapItemType {
7+
ACTIVITY = "activity",
8+
PLAYTIME = "playtime",
9+
}

src/journal/journal.controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SessionContainer } from "supertokens-node/recipe/session";
66
import { JournalService } from "./journal.service";
77
import { ApiTags } from "@nestjs/swagger";
88
import { JournalPlayLogService } from "./journal-play-log.service";
9+
import { JournalHeatmapService } from "./journal-heatmap.service";
910

1011
@Controller("journal")
1112
@ApiTags("journal")
@@ -14,6 +15,7 @@ export class JournalController {
1415
constructor(
1516
private readonly journalService: JournalService,
1617
private readonly journalPlaylogService: JournalPlayLogService,
18+
private readonly journalHeatmapService: JournalHeatmapService,
1719
) {}
1820

1921
@Get("overview/:userId")
@@ -33,4 +35,16 @@ export class JournalController {
3335
) {
3436
return this.journalPlaylogService.getPlaylog(userId, gameId);
3537
}
38+
39+
@Get("heatmap/:userId")
40+
@Public()
41+
public async getHeatmap(
42+
@Session() session: SessionContainer | undefined,
43+
@Param("userId") targetUserId: string,
44+
) {
45+
return this.journalHeatmapService.buildHeatmap(
46+
session?.getUserId(),
47+
targetUserId,
48+
);
49+
}
3650
}

0 commit comments

Comments
 (0)