diff --git a/.claude/agents/ai-engineer.md b/.agent/agents/ai-engineer.md similarity index 100% rename from .claude/agents/ai-engineer.md rename to .agent/agents/ai-engineer.md diff --git a/.claude/agents/api-documenter.md b/.agent/agents/api-documenter.md similarity index 100% rename from .claude/agents/api-documenter.md rename to .agent/agents/api-documenter.md diff --git a/.claude/agents/api-security-audit.md b/.agent/agents/api-security-audit.md similarity index 100% rename from .claude/agents/api-security-audit.md rename to .agent/agents/api-security-audit.md diff --git a/.claude/agents/code-reviewer.md b/.agent/agents/code-reviewer.md similarity index 100% rename from .claude/agents/code-reviewer.md rename to .agent/agents/code-reviewer.md diff --git a/.claude/agents/data-scientist.md b/.agent/agents/data-scientist.md similarity index 100% rename from .claude/agents/data-scientist.md rename to .agent/agents/data-scientist.md diff --git a/.claude/agents/database-optimizer.md b/.agent/agents/database-optimizer.md similarity index 100% rename from .claude/agents/database-optimizer.md rename to .agent/agents/database-optimizer.md diff --git a/.claude/agents/debugger.md b/.agent/agents/debugger.md similarity index 100% rename from .claude/agents/debugger.md rename to .agent/agents/debugger.md diff --git a/.claude/agents/security-engineer.md b/.agent/agents/security-engineer.md similarity index 100% rename from .claude/agents/security-engineer.md rename to .agent/agents/security-engineer.md diff --git a/.claude/agents/typescript-pro.md b/.agent/agents/typescript-pro.md similarity index 100% rename from .claude/agents/typescript-pro.md rename to .agent/agents/typescript-pro.md diff --git a/.claude/skills/code-reviewer/SKILL.md b/.agent/skills/code-reviewer/SKILL.md similarity index 100% rename from .claude/skills/code-reviewer/SKILL.md rename to .agent/skills/code-reviewer/SKILL.md diff --git a/.claude/skills/code-reviewer/references/code_review_checklist.md b/.agent/skills/code-reviewer/references/code_review_checklist.md similarity index 100% rename from .claude/skills/code-reviewer/references/code_review_checklist.md rename to .agent/skills/code-reviewer/references/code_review_checklist.md diff --git a/.claude/skills/code-reviewer/references/coding_standards.md b/.agent/skills/code-reviewer/references/coding_standards.md similarity index 100% rename from .claude/skills/code-reviewer/references/coding_standards.md rename to .agent/skills/code-reviewer/references/coding_standards.md diff --git a/.claude/skills/code-reviewer/references/common_antipatterns.md b/.agent/skills/code-reviewer/references/common_antipatterns.md similarity index 100% rename from .claude/skills/code-reviewer/references/common_antipatterns.md rename to .agent/skills/code-reviewer/references/common_antipatterns.md diff --git a/.claude/skills/code-reviewer/scripts/code_quality_checker.py b/.agent/skills/code-reviewer/scripts/code_quality_checker.py similarity index 100% rename from .claude/skills/code-reviewer/scripts/code_quality_checker.py rename to .agent/skills/code-reviewer/scripts/code_quality_checker.py diff --git a/.claude/skills/code-reviewer/scripts/pr_analyzer.py b/.agent/skills/code-reviewer/scripts/pr_analyzer.py similarity index 100% rename from .claude/skills/code-reviewer/scripts/pr_analyzer.py rename to .agent/skills/code-reviewer/scripts/pr_analyzer.py diff --git a/.claude/skills/code-reviewer/scripts/review_report_generator.py b/.agent/skills/code-reviewer/scripts/review_report_generator.py similarity index 100% rename from .claude/skills/code-reviewer/scripts/review_report_generator.py rename to .agent/skills/code-reviewer/scripts/review_report_generator.py diff --git a/.claude/skills/receiving-code-review/SKILL.md b/.agent/skills/receiving-code-review/SKILL.md similarity index 100% rename from .claude/skills/receiving-code-review/SKILL.md rename to .agent/skills/receiving-code-review/SKILL.md diff --git a/.claude/skills/senior-backend/SKILL.md b/.agent/skills/senior-backend/SKILL.md similarity index 100% rename from .claude/skills/senior-backend/SKILL.md rename to .agent/skills/senior-backend/SKILL.md diff --git a/.claude/skills/senior-backend/references/api_design_patterns.md b/.agent/skills/senior-backend/references/api_design_patterns.md similarity index 100% rename from .claude/skills/senior-backend/references/api_design_patterns.md rename to .agent/skills/senior-backend/references/api_design_patterns.md diff --git a/.claude/skills/senior-backend/references/backend_security_practices.md b/.agent/skills/senior-backend/references/backend_security_practices.md similarity index 100% rename from .claude/skills/senior-backend/references/backend_security_practices.md rename to .agent/skills/senior-backend/references/backend_security_practices.md diff --git a/.claude/skills/senior-backend/references/database_optimization_guide.md b/.agent/skills/senior-backend/references/database_optimization_guide.md similarity index 100% rename from .claude/skills/senior-backend/references/database_optimization_guide.md rename to .agent/skills/senior-backend/references/database_optimization_guide.md diff --git a/.claude/skills/senior-backend/scripts/api_load_tester.py b/.agent/skills/senior-backend/scripts/api_load_tester.py similarity index 100% rename from .claude/skills/senior-backend/scripts/api_load_tester.py rename to .agent/skills/senior-backend/scripts/api_load_tester.py diff --git a/.claude/skills/senior-backend/scripts/api_scaffolder.py b/.agent/skills/senior-backend/scripts/api_scaffolder.py similarity index 100% rename from .claude/skills/senior-backend/scripts/api_scaffolder.py rename to .agent/skills/senior-backend/scripts/api_scaffolder.py diff --git a/.claude/skills/senior-backend/scripts/database_migration_tool.py b/.agent/skills/senior-backend/scripts/database_migration_tool.py similarity index 100% rename from .claude/skills/senior-backend/scripts/database_migration_tool.py rename to .agent/skills/senior-backend/scripts/database_migration_tool.py diff --git a/.claude/skills/senior-fullstack/SKILL.md b/.agent/skills/senior-fullstack/SKILL.md similarity index 100% rename from .claude/skills/senior-fullstack/SKILL.md rename to .agent/skills/senior-fullstack/SKILL.md diff --git a/.claude/skills/senior-fullstack/references/architecture_patterns.md b/.agent/skills/senior-fullstack/references/architecture_patterns.md similarity index 100% rename from .claude/skills/senior-fullstack/references/architecture_patterns.md rename to .agent/skills/senior-fullstack/references/architecture_patterns.md diff --git a/.claude/skills/senior-fullstack/references/development_workflows.md b/.agent/skills/senior-fullstack/references/development_workflows.md similarity index 100% rename from .claude/skills/senior-fullstack/references/development_workflows.md rename to .agent/skills/senior-fullstack/references/development_workflows.md diff --git a/.claude/skills/senior-fullstack/references/tech_stack_guide.md b/.agent/skills/senior-fullstack/references/tech_stack_guide.md similarity index 100% rename from .claude/skills/senior-fullstack/references/tech_stack_guide.md rename to .agent/skills/senior-fullstack/references/tech_stack_guide.md diff --git a/.claude/skills/senior-fullstack/scripts/code_quality_analyzer.py b/.agent/skills/senior-fullstack/scripts/code_quality_analyzer.py similarity index 100% rename from .claude/skills/senior-fullstack/scripts/code_quality_analyzer.py rename to .agent/skills/senior-fullstack/scripts/code_quality_analyzer.py diff --git a/.claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/.agent/skills/senior-fullstack/scripts/fullstack_scaffolder.py similarity index 100% rename from .claude/skills/senior-fullstack/scripts/fullstack_scaffolder.py rename to .agent/skills/senior-fullstack/scripts/fullstack_scaffolder.py diff --git a/.claude/skills/senior-fullstack/scripts/project_scaffolder.py b/.agent/skills/senior-fullstack/scripts/project_scaffolder.py similarity index 100% rename from .claude/skills/senior-fullstack/scripts/project_scaffolder.py rename to .agent/skills/senior-fullstack/scripts/project_scaffolder.py diff --git a/.claude/skills/senior-ml-engineer/SKILL.md b/.agent/skills/senior-ml-engineer/SKILL.md similarity index 100% rename from .claude/skills/senior-ml-engineer/SKILL.md rename to .agent/skills/senior-ml-engineer/SKILL.md diff --git a/.claude/skills/senior-ml-engineer/references/llm_integration_guide.md b/.agent/skills/senior-ml-engineer/references/llm_integration_guide.md similarity index 100% rename from .claude/skills/senior-ml-engineer/references/llm_integration_guide.md rename to .agent/skills/senior-ml-engineer/references/llm_integration_guide.md diff --git a/.claude/skills/senior-ml-engineer/references/mlops_production_patterns.md b/.agent/skills/senior-ml-engineer/references/mlops_production_patterns.md similarity index 100% rename from .claude/skills/senior-ml-engineer/references/mlops_production_patterns.md rename to .agent/skills/senior-ml-engineer/references/mlops_production_patterns.md diff --git a/.claude/skills/senior-ml-engineer/references/rag_system_architecture.md b/.agent/skills/senior-ml-engineer/references/rag_system_architecture.md similarity index 100% rename from .claude/skills/senior-ml-engineer/references/rag_system_architecture.md rename to .agent/skills/senior-ml-engineer/references/rag_system_architecture.md diff --git a/.claude/skills/senior-ml-engineer/scripts/ml_monitoring_suite.py b/.agent/skills/senior-ml-engineer/scripts/ml_monitoring_suite.py similarity index 100% rename from .claude/skills/senior-ml-engineer/scripts/ml_monitoring_suite.py rename to .agent/skills/senior-ml-engineer/scripts/ml_monitoring_suite.py diff --git a/.claude/skills/senior-ml-engineer/scripts/model_deployment_pipeline.py b/.agent/skills/senior-ml-engineer/scripts/model_deployment_pipeline.py similarity index 100% rename from .claude/skills/senior-ml-engineer/scripts/model_deployment_pipeline.py rename to .agent/skills/senior-ml-engineer/scripts/model_deployment_pipeline.py diff --git a/.claude/skills/senior-ml-engineer/scripts/rag_system_builder.py b/.agent/skills/senior-ml-engineer/scripts/rag_system_builder.py similarity index 100% rename from .claude/skills/senior-ml-engineer/scripts/rag_system_builder.py rename to .agent/skills/senior-ml-engineer/scripts/rag_system_builder.py diff --git a/.claude/skills/senior-prompt-engineer/SKILL.md b/.agent/skills/senior-prompt-engineer/SKILL.md similarity index 100% rename from .claude/skills/senior-prompt-engineer/SKILL.md rename to .agent/skills/senior-prompt-engineer/SKILL.md diff --git a/.claude/skills/senior-prompt-engineer/references/agentic_system_design.md b/.agent/skills/senior-prompt-engineer/references/agentic_system_design.md similarity index 100% rename from .claude/skills/senior-prompt-engineer/references/agentic_system_design.md rename to .agent/skills/senior-prompt-engineer/references/agentic_system_design.md diff --git a/.claude/skills/senior-prompt-engineer/references/llm_evaluation_frameworks.md b/.agent/skills/senior-prompt-engineer/references/llm_evaluation_frameworks.md similarity index 100% rename from .claude/skills/senior-prompt-engineer/references/llm_evaluation_frameworks.md rename to .agent/skills/senior-prompt-engineer/references/llm_evaluation_frameworks.md diff --git a/.claude/skills/senior-prompt-engineer/references/prompt_engineering_patterns.md b/.agent/skills/senior-prompt-engineer/references/prompt_engineering_patterns.md similarity index 100% rename from .claude/skills/senior-prompt-engineer/references/prompt_engineering_patterns.md rename to .agent/skills/senior-prompt-engineer/references/prompt_engineering_patterns.md diff --git a/.claude/skills/senior-prompt-engineer/scripts/agent_orchestrator.py b/.agent/skills/senior-prompt-engineer/scripts/agent_orchestrator.py similarity index 100% rename from .claude/skills/senior-prompt-engineer/scripts/agent_orchestrator.py rename to .agent/skills/senior-prompt-engineer/scripts/agent_orchestrator.py diff --git a/.claude/skills/senior-prompt-engineer/scripts/prompt_optimizer.py b/.agent/skills/senior-prompt-engineer/scripts/prompt_optimizer.py similarity index 100% rename from .claude/skills/senior-prompt-engineer/scripts/prompt_optimizer.py rename to .agent/skills/senior-prompt-engineer/scripts/prompt_optimizer.py diff --git a/.claude/skills/senior-prompt-engineer/scripts/rag_evaluator.py b/.agent/skills/senior-prompt-engineer/scripts/rag_evaluator.py similarity index 100% rename from .claude/skills/senior-prompt-engineer/scripts/rag_evaluator.py rename to .agent/skills/senior-prompt-engineer/scripts/rag_evaluator.py diff --git a/deploy.md b/deploy.md new file mode 100644 index 0000000..3aa55f7 --- /dev/null +++ b/deploy.md @@ -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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 84a8ee3..3bcf69e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7f640d4..a4325bd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/migrations/20260130001023_init_game_calendar/migration.sql b/prisma/migrations/20260130001023_init_game_calendar/migration.sql new file mode 100644 index 0000000..1710665 --- /dev/null +++ b/prisma/migrations/20260130001023_init_game_calendar/migration.sql @@ -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; diff --git a/src/app.module.ts b/src/app.module.ts index 7db004a..632f187 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 { } diff --git a/src/modules/events/dto/create-event.dto.ts b/src/modules/events/dto/create-event.dto.ts new file mode 100644 index 0000000..b2345a7 --- /dev/null +++ b/src/modules/events/dto/create-event.dto.ts @@ -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; +} diff --git a/src/modules/events/dto/update-event.dto.ts b/src/modules/events/dto/update-event.dto.ts new file mode 100644 index 0000000..451cb4b --- /dev/null +++ b/src/modules/events/dto/update-event.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateEventDto } from './create-event.dto'; + +export class UpdateEventDto extends PartialType(CreateEventDto) { } diff --git a/src/modules/events/events.controller.ts b/src/modules/events/events.controller.ts new file mode 100644 index 0000000..244e7f9 --- /dev/null +++ b/src/modules/events/events.controller.ts @@ -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 { + constructor(protected readonly eventsService: EventsService) { + super(eventsService, 'Event'); + } +} diff --git a/src/modules/events/events.module.ts b/src/modules/events/events.module.ts new file mode 100644 index 0000000..2cebbfe --- /dev/null +++ b/src/modules/events/events.module.ts @@ -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 { } diff --git a/src/modules/events/events.service.ts b/src/modules/events/events.service.ts new file mode 100644 index 0000000..e94910f --- /dev/null +++ b/src/modules/events/events.service.ts @@ -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 { + constructor(protected readonly prisma: PrismaService) { + super(prisma, 'Event'); + } +} diff --git a/src/modules/external-api/external-api.module.ts b/src/modules/external-api/external-api.module.ts new file mode 100644 index 0000000..c1f2654 --- /dev/null +++ b/src/modules/external-api/external-api.module.ts @@ -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 { } diff --git a/src/modules/external-api/external-api.service.ts b/src/modules/external-api/external-api.service.ts new file mode 100644 index 0000000..f1a8a3c --- /dev/null +++ b/src/modules/external-api/external-api.service.ts @@ -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); + } +} diff --git a/src/modules/external-api/interfaces/game-provider.interface.ts b/src/modules/external-api/interfaces/game-provider.interface.ts new file mode 100644 index 0000000..612c0bd --- /dev/null +++ b/src/modules/external-api/interfaces/game-provider.interface.ts @@ -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; + abstract getGameDetails(slug: string): Promise; + abstract getUpcomingGames(limit: number): Promise; +} diff --git a/src/modules/external-api/providers/igdb.provider.ts b/src/modules/external-api/providers/igdb.provider.ts new file mode 100644 index 0000000..424ff8c --- /dev/null +++ b/src/modules/external-api/providers/igdb.provider.ts @@ -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 { + this.logger.log(`Searching games for query: ${query}`); + // TODO: Implement actual IGDB call + return []; + } + + async getGameDetails(slug: string): Promise { + this.logger.log(`Fetching details for game: ${slug}`); + // TODO: Implement actual IGDB call + return null; + } + + async getUpcomingGames(limit: number): Promise { + this.logger.log(`Fetching upcoming ${limit} games`); + // TODO: Implement actual IGDB call + return []; + } +} diff --git a/src/modules/games/dto/create-game.dto.ts b/src/modules/games/dto/create-game.dto.ts new file mode 100644 index 0000000..c21c14e --- /dev/null +++ b/src/modules/games/dto/create-game.dto.ts @@ -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; +} diff --git a/src/modules/games/dto/game-response.dto.ts b/src/modules/games/dto/game-response.dto.ts new file mode 100644 index 0000000..0aed262 --- /dev/null +++ b/src/modules/games/dto/game-response.dto.ts @@ -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; +} diff --git a/src/modules/games/dto/update-game.dto.ts b/src/modules/games/dto/update-game.dto.ts new file mode 100644 index 0000000..00f43a6 --- /dev/null +++ b/src/modules/games/dto/update-game.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateGameDto } from './create-game.dto'; + +export class UpdateGameDto extends PartialType(CreateGameDto) { } diff --git a/src/modules/games/games.controller.ts b/src/modules/games/games.controller.ts new file mode 100644 index 0000000..001692d --- /dev/null +++ b/src/modules/games/games.controller.ts @@ -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 { + 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'); + } +} diff --git a/src/modules/games/games.module.ts b/src/modules/games/games.module.ts new file mode 100644 index 0000000..48c7a8a --- /dev/null +++ b/src/modules/games/games.module.ts @@ -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 { } diff --git a/src/modules/games/games.service.ts b/src/modules/games/games.service.ts new file mode 100644 index 0000000..2734362 --- /dev/null +++ b/src/modules/games/games.service.ts @@ -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 { + 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; + } +}