76
deploy.md
Normal file
76
deploy.md
Normal 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
60
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -75,11 +78,11 @@ import {
|
||||
level: configService.get('app.isDevelopment') ? 'debug' : 'info',
|
||||
transport: configService.get('app.isDevelopment')
|
||||
? {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
@@ -160,6 +163,9 @@ import {
|
||||
// Optional Modules (controlled by env variables)
|
||||
GeminiModule,
|
||||
HealthModule,
|
||||
ExternalApiModule,
|
||||
GamesModule,
|
||||
EventsModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Exception Filter
|
||||
@@ -199,4 +205,4 @@ import {
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
44
src/modules/events/dto/create-event.dto.ts
Normal file
44
src/modules/events/dto/create-event.dto.ts
Normal 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;
|
||||
}
|
||||
4
src/modules/events/dto/update-event.dto.ts
Normal file
4
src/modules/events/dto/update-event.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateEventDto } from './create-event.dto';
|
||||
|
||||
export class UpdateEventDto extends PartialType(CreateEventDto) { }
|
||||
15
src/modules/events/events.controller.ts
Normal file
15
src/modules/events/events.controller.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
10
src/modules/events/events.module.ts
Normal file
10
src/modules/events/events.module.ts
Normal 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 { }
|
||||
13
src/modules/events/events.service.ts
Normal file
13
src/modules/events/events.service.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
18
src/modules/external-api/external-api.module.ts
Normal file
18
src/modules/external-api/external-api.module.ts
Normal 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 { }
|
||||
22
src/modules/external-api/external-api.service.ts
Normal file
22
src/modules/external-api/external-api.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
25
src/modules/external-api/providers/igdb.provider.ts
Normal file
25
src/modules/external-api/providers/igdb.provider.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
49
src/modules/games/dto/create-game.dto.ts
Normal file
49
src/modules/games/dto/create-game.dto.ts
Normal 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;
|
||||
}
|
||||
45
src/modules/games/dto/game-response.dto.ts
Normal file
45
src/modules/games/dto/game-response.dto.ts
Normal 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;
|
||||
}
|
||||
4
src/modules/games/dto/update-game.dto.ts
Normal file
4
src/modules/games/dto/update-game.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateGameDto } from './create-game.dto';
|
||||
|
||||
export class UpdateGameDto extends PartialType(CreateGameDto) { }
|
||||
23
src/modules/games/games.controller.ts
Normal file
23
src/modules/games/games.controller.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
12
src/modules/games/games.module.ts
Normal file
12
src/modules/games/games.module.ts
Normal 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 { }
|
||||
50
src/modules/games/games.service.ts
Normal file
50
src/modules/games/games.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user