main
Some checks failed
CI / build (push) Failing after 59s

This commit is contained in:
Harun CAN
2026-01-30 03:41:08 +03:00
parent 15f57dcb08
commit 8995e79e78
65 changed files with 643 additions and 15 deletions

76
deploy.md Normal file
View File

@@ -0,0 +1,76 @@
🏗️ Backend Altyapı Kurulum Rehberi (Database & Redis)
Bu doküman, Raspberry Pi üzerinde yeni bir Backend projesi için gerekli olan kalıcı veritabanı ve Redis servislerinin nasıl kurulacağını anlatır.
⚠️ Mantık: Bu servisler deploy sürecine dahil EDİLMEZ. Sunucuda bir kere kurulur, verileri kalıcı olarak saklar ve Backend projesi buraya bağlanır.
1. Hazırlık: Docker Ağı Kontrolü
Tüm servislerin (Gitea, App, DB, Redis) birbirini görebilmesi için ortak bir ağda olmaları gerekir.
Bash
# Ağ var mı kontrol et (Listede 'gitea' yazmalı)
docker network ls
# Yoksa oluştur:
docker network create gitea
2. PostgreSQL Veritabanı Kurulumu (Kalıcı)
Her yeni proje için port çakışması yaşamamak adına konteyner ismini ve volume ismini projeye özel değiştir.
Değiştirilecek Yerler: proje-db-ismi, DB_KULLANICI, DB_SIFRE, DB_ADI
Bash
docker run -d \
--name proje-adi-postgres \
--restart always \
--network gitea \
-e POSTGRES_USER=db_kullanici \
-e POSTGRES_PASSWORD=cok_guclu_sifre \
-e POSTGRES_DB=proje_db_adi \
-v proje_adi_db_data:/var/lib/postgresql/data \
postgres:16-alpine
Not: -p (Port) parametresi eklemedik. Çünkü dış dünyaya kapalı olsun, sadece bizim uygulamamız (aynı ağdaki) erişebilsin istiyoruz. Güvenlik için en iyisi budur.
3. Redis Kurulumu (Kalıcı)
Redis için de projeye özel bir isim veriyoruz.
Bash
docker run -d \
--name proje-adi-redis \
--restart always \
--network gitea \
-v proje_adi_redis_data:/data \
redis:7-alpine
4. Gitea Secrets Ayarları (Bağlantı)
Veritabanlarını kurduktan sonra Gitea'da Ayarlar -> Actions -> Secrets kısmına gidip aşağıdaki bilgileri ekle.
🔑 Secret 1: DATABASE_URL
Uygulamanın veritabanını bulması için gerekli bağlantı cümlesi.
Format: postgresql://KULLANICI:SIFRE@KONTEYNER_ADI:5432/DB_ADI?schema=public
Örnek (Yukarıdaki kuruluma göre): postgresql://db_kullanici:cok_guclu_sifre@proje-adi-postgres:5432/proje_db_adi?schema=public
Dikkat: localhost veya IP yerine direkt kurduğun konteyner ismini (proje-adi-postgres) yazıyoruz. Docker isimden tanır.
🔑 Secret 2: REDIS_HOST
Uygulamanın Redis'i bulması için.
Değer: proje-adi-redis
5. Sorun Giderme (Debug)
Eğer bağlantı hatası alırsan şu komutlarla kontrol et:
Veritabanı ayakta mı?
Bash
docker ps | grep postgres
Veritabanı loglarını incele:
Bash
docker logs --tail 50 proje-adi-postgres
Veritabanını sıfırlamak (Silip baştan kurmak) istersen:
Bash
# DİKKAT: TÜM VERİ SİLİNİR!
docker rm -f proje-adi-postgres
docker volume rm proje_adi_db_data

60
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.964.0",
"@google/genai": "^1.35.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.0.1",
@@ -24,6 +25,7 @@
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.22.0",
"axios": "^1.13.4",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
"cache-manager": "^7.2.7",
@@ -2987,6 +2989,17 @@
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@nestjs/axios": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"axios": "^1.3.1",
"rxjs": "^7.0.0"
}
},
"node_modules/@nestjs/bull-shared": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz",
@@ -5831,8 +5844,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
@@ -5842,6 +5854,17 @@
"node": ">=8.0.0"
}
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
@@ -6645,7 +6668,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -6913,7 +6935,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -7193,7 +7214,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@@ -7778,6 +7798,26 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -7824,7 +7864,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -7840,7 +7879,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -7849,7 +7887,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -8225,7 +8262,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -10834,6 +10870,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",

View File

@@ -21,6 +21,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.964.0",
"@google/genai": "^1.35.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.0.1",
@@ -34,6 +35,7 @@
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^5.22.0",
"axios": "^1.13.4",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
"cache-manager": "^7.2.7",

View File

@@ -0,0 +1,144 @@
-- CreateEnum
CREATE TYPE "EventType" AS ENUM ('SHOWCASE', 'RELEASE', 'TOURNAMENT', 'OTHER');
-- CreateTable
CREATE TABLE "Game" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"coverImage" TEXT,
"description" TEXT,
"releaseDate" TIMESTAMP(3),
"isTBD" BOOLEAN NOT NULL DEFAULT false,
"releaseDateText" TEXT,
"igdbId" INTEGER,
"rawgId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "Game_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Platform" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"icon" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Platform_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GamePlatform" (
"gameId" TEXT NOT NULL,
"platformId" TEXT NOT NULL,
CONSTRAINT "GamePlatform_pkey" PRIMARY KEY ("gameId","platformId")
);
-- CreateTable
CREATE TABLE "Event" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"startTime" TIMESTAMP(3) NOT NULL,
"endTime" TIMESTAMP(3),
"streamUrl" TEXT,
"coverImage" TEXT,
"type" "EventType" NOT NULL DEFAULT 'SHOWCASE',
"source" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Subscription" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"gameId" TEXT,
"eventId" TEXT,
"notifyEmail" BOOLEAN NOT NULL DEFAULT false,
"notifyPush" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ThemeConfig" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL DEFAULT 'current_theme',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"gameTitle" TEXT NOT NULL,
"primaryColor" TEXT NOT NULL,
"secondaryColor" TEXT NOT NULL,
"backgroundColor" TEXT NOT NULL,
"backgroundImage" TEXT,
"logoImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ThemeConfig_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Game_slug_key" ON "Game"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Game_igdbId_key" ON "Game"("igdbId");
-- CreateIndex
CREATE UNIQUE INDEX "Game_rawgId_key" ON "Game"("rawgId");
-- CreateIndex
CREATE INDEX "Game_releaseDate_idx" ON "Game"("releaseDate");
-- CreateIndex
CREATE INDEX "Game_slug_idx" ON "Game"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Platform_name_key" ON "Platform"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Platform_slug_key" ON "Platform"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Event_slug_key" ON "Event"("slug");
-- CreateIndex
CREATE INDEX "Event_startTime_idx" ON "Event"("startTime");
-- CreateIndex
CREATE INDEX "Subscription_userId_idx" ON "Subscription"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_gameId_key" ON "Subscription"("userId", "gameId");
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_eventId_key" ON "Subscription"("userId", "eventId");
-- CreateIndex
CREATE UNIQUE INDEX "ThemeConfig_key_key" ON "ThemeConfig"("key");
-- AddForeignKey
ALTER TABLE "GamePlatform" ADD CONSTRAINT "GamePlatform_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GamePlatform" ADD CONSTRAINT "GamePlatform_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "Platform"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -39,6 +39,9 @@ import { UsersModule } from './modules/users/users.module';
import { AdminModule } from './modules/admin/admin.module';
import { HealthModule } from './modules/health/health.module';
import { GeminiModule } from './modules/gemini/gemini.module';
import { ExternalApiModule } from './modules/external-api/external-api.module';
import { GamesModule } from './modules/games/games.module';
import { EventsModule } from './modules/events/events.module';
// Guards
import {
@@ -160,6 +163,9 @@ import {
// Optional Modules (controlled by env variables)
GeminiModule,
HealthModule,
ExternalApiModule,
GamesModule,
EventsModule,
],
providers: [
// Global Exception Filter

View File

@@ -0,0 +1,44 @@
import { IsString, IsNotEmpty, IsOptional, IsDateString, IsEnum, IsUrl } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { EventType } from '@prisma/client';
export class CreateEventDto {
@ApiProperty({ example: 'PlayStation Showcase 2026' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ example: 'playstation-showcase-2026' })
@IsString()
@IsNotEmpty()
slug: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ example: '2026-06-12T20:00:00Z' })
@IsDateString()
startTime: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
endTime?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsUrl()
streamUrl?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsUrl()
coverImage?: string;
@ApiProperty({ enum: EventType, default: EventType.SHOWCASE })
@IsOptional()
@IsEnum(EventType)
type?: EventType;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateEventDto } from './create-event.dto';
export class UpdateEventDto extends PartialType(CreateEventDto) { }

View File

@@ -0,0 +1,15 @@
import { Controller } from '@nestjs/common';
import { BaseController } from '../../common/base/base.controller';
import { Event } from '@prisma/client';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { EventsService } from './events.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('Events')
@Controller('events')
export class EventsController extends BaseController<Event, CreateEventDto, UpdateEventDto> {
constructor(protected readonly eventsService: EventsService) {
super(eventsService, 'Event');
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';
@Module({
controllers: [EventsController],
providers: [EventsService],
exports: [EventsService],
})
export class EventsModule { }

View File

@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { BaseService } from '../../common/base/base.service';
import { Event } from '@prisma/client';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { PrismaService } from '../../database/prisma.service';
@Injectable()
export class EventsService extends BaseService<Event, CreateEventDto, UpdateEventDto> {
constructor(protected readonly prisma: PrismaService) {
super(prisma, 'Event');
}
}

View File

@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ExternalApiService } from './external-api.service';
import { IgdbProvider } from './providers/igdb.provider';
import { HttpModule } from '@nestjs/axios';
import { GameDataProvider } from './interfaces/game-provider.interface';
@Module({
imports: [HttpModule],
providers: [
ExternalApiService,
{
provide: GameDataProvider,
useClass: IgdbProvider,
}
],
exports: [ExternalApiService],
})
export class ExternalApiModule { }

View File

@@ -0,0 +1,22 @@
import { Injectable, Inject } from '@nestjs/common';
import { GameDataProvider } from './interfaces/game-provider.interface';
import { IgdbProvider } from './providers/igdb.provider';
@Injectable()
export class ExternalApiService {
constructor(
private readonly gameProvider: GameDataProvider,
) { }
async search(query: string) {
return this.gameProvider.searchGames(query);
}
async getUpcoming() {
return this.gameProvider.getUpcomingGames(10);
}
async getDetails(slug: string) {
return this.gameProvider.getGameDetails(slug);
}
}

View File

@@ -0,0 +1,24 @@
export interface GameSearchResult {
slug: string;
name: string;
coverUrl?: string;
firstReleaseDate?: Date;
}
export interface GameDetails {
slug: string;
name: string;
description?: string;
coverUrl?: string;
firstReleaseDate?: Date;
platforms: string[]; // Platform slugs
screenshots?: string[];
externalId: number; // Provider specific ID
}
export abstract class GameDataProvider {
abstract searchGames(query: string): Promise<GameSearchResult[]>;
abstract getGameDetails(slug: string): Promise<GameDetails | null>;
abstract getUpcomingGames(limit: number): Promise<GameSearchResult[]>;
}

View File

@@ -0,0 +1,25 @@
import { Injectable, Logger } from '@nestjs/common';
import { GameDataProvider, GameDetails, GameSearchResult } from '../interfaces/game-provider.interface';
@Injectable()
export class IgdbProvider implements GameDataProvider {
private readonly logger = new Logger(IgdbProvider.name);
async searchGames(query: string): Promise<GameSearchResult[]> {
this.logger.log(`Searching games for query: ${query}`);
// TODO: Implement actual IGDB call
return [];
}
async getGameDetails(slug: string): Promise<GameDetails | null> {
this.logger.log(`Fetching details for game: ${slug}`);
// TODO: Implement actual IGDB call
return null;
}
async getUpcomingGames(limit: number): Promise<GameSearchResult[]> {
this.logger.log(`Fetching upcoming ${limit} games`);
// TODO: Implement actual IGDB call
return [];
}
}

View File

@@ -0,0 +1,49 @@
import { IsString, IsNotEmpty, IsOptional, IsDateString, IsBoolean, IsUrl, IsInt } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateGameDto {
@ApiProperty({ example: 'Elden Ring' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ example: 'elden-ring', description: 'Unique slug' })
@IsString()
@IsNotEmpty()
slug: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ required: false, example: 'https://cover.url' })
@IsOptional()
@IsUrl()
coverImage?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
releaseDate?: string;
@ApiProperty({ default: false })
@IsOptional()
@IsBoolean()
isTBD?: boolean;
@ApiProperty({ required: false, example: '2022' })
@IsOptional()
@IsString()
releaseDateText?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsInt()
igdbId?: number;
@ApiProperty({ required: false })
@IsOptional()
@IsInt()
rawgId?: number;
}

View File

@@ -0,0 +1,45 @@
import { Exclude, Expose } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
@Exclude()
export class GameResponseDto {
@Expose()
@ApiProperty()
id: string;
@Expose()
@ApiProperty()
title: string;
@Expose()
@ApiProperty()
slug: string;
@Expose()
@ApiProperty()
description?: string;
@Expose()
@ApiProperty()
coverImage?: string;
@Expose()
@ApiProperty()
releaseDate?: Date;
@Expose()
@ApiProperty()
isTBD: boolean;
@Expose()
@ApiProperty()
releaseDateText?: string;
@Expose()
@ApiProperty()
createdAt: Date;
@Expose()
@ApiProperty()
updatedAt: Date;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateGameDto } from './create-game.dto';
export class UpdateGameDto extends PartialType(CreateGameDto) { }

View File

@@ -0,0 +1,23 @@
import { Controller, Post, Body, Param } from '@nestjs/common';
import { BaseController } from '../../common/base/base.controller';
import { Game } from '@prisma/client';
import { CreateGameDto } from './dto/create-game.dto';
import { UpdateGameDto } from './dto/update-game.dto';
import { GamesService } from './games.service';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { createSuccessResponse } from '../../common/types/api-response.type';
@ApiTags('Games')
@Controller('games')
export class GamesController extends BaseController<Game, CreateGameDto, UpdateGameDto> {
constructor(protected readonly gamesService: GamesService) {
super(gamesService, 'Game');
}
@Post(':slug/sync')
@ApiOperation({ summary: 'Sync game data from external provider' })
async syncGame(@Param('slug') slug: string) {
const result = await this.gamesService.syncWithExternal(slug);
return createSuccessResponse(result, 'Game synced successfully');
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { GamesController } from './games.controller';
import { GamesService } from './games.service';
import { ExternalApiModule } from '../external-api/external-api.module';
@Module({
imports: [ExternalApiModule],
controllers: [GamesController],
providers: [GamesService],
exports: [GamesService],
})
export class GamesModule { }

View File

@@ -0,0 +1,50 @@
import { Injectable, Logger } from '@nestjs/common';
import { BaseService } from '../../common/base/base.service';
import { Game, Prisma } from '@prisma/client';
import { CreateGameDto } from './dto/create-game.dto';
import { UpdateGameDto } from './dto/update-game.dto';
import { PrismaService } from '../../database/prisma.service';
import { ExternalApiService } from '../external-api/external-api.service';
@Injectable()
export class GamesService extends BaseService<Game, CreateGameDto, UpdateGameDto> {
constructor(
protected readonly prisma: PrismaService,
private readonly externalApiService: ExternalApiService,
) {
super(prisma, 'Game');
}
async syncWithExternal(slug: string) {
this.logger.log(`Syncing game ${slug} with external provider...`);
// 1. Fetch details
const details = await this.externalApiService.getDetails(slug);
if (!details) {
this.logger.warn(`Game ${slug} not found in external provider`);
return null;
}
// 2. Upsert game
const game = await this.prisma.game.upsert({
where: { slug: details.slug },
update: {
title: details.name,
description: details.description,
coverImage: details.coverUrl,
releaseDate: details.firstReleaseDate,
igdbId: details.externalId,
},
create: {
slug: details.slug,
title: details.name,
description: details.description,
coverImage: details.coverUrl,
releaseDate: details.firstReleaseDate,
igdbId: details.externalId,
},
});
return game;
}
}