modules/ai-guide/Write-AiGuide-Frontend.ps1
|
param( [string] $ProjectName, [string] $OrmMode, [string] $DbProvider = "MSSQL", [string] $FrontendMode, [string[]] $Entities = @("User", "Product"), [string] $OutputPath, [bool] $SkipTests = $false, [bool] $SkipCi = $false, [bool] $SkipFrontend = $false, [bool] $Observability = $false, [string] $IdeConfig = "VSCode", [bool] $I18n = $false, [string] $Platform = "Web" ) . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1") $guideDir = Join-Path $OutputPath "ai-guide" function Write-Guide($fileName, $content) { Write-CoreFile (Join-Path $guideDir $fileName) $content } $nonAuthEntities = @($Entities | Where-Object { $_ -ne "User" }) $firstE = if ($nonAuthEntities.Count -gt 0) { $nonAuthEntities[0] } else { "Product" } $firstPE = Get-Plural $firstE $firstLowE = $firstE.ToLower() # ─── testing-guide.md ───────────────────────────────────── if (-not $SkipTests) { Write-Guide "testing-guide.md" @" # Test Yazma Rehberi Projede 3 test katmani bulunur: **Domain.Tests**, **Application.Tests**, **Integration.Tests** ## Test Calistirma ``````bash dotnet test # Tum testler dotnet test --filter "Category=Unit" # Sadece unit testler dotnet test --logger "console;verbosity=normal" `````` --- ## Domain.Tests — Saf Unit Test Domain entity'lerinin davranisini test eder. Dis bagimlilik yoktur. ``````csharp // tests/$ProjectName.Domain.Tests/Entities/${firstE}Tests.cs public class ${firstE}Tests { [Fact] public void New${firstE}_ShouldHave_DefaultValues() { var entity = new $firstE { Name = "Test" }; entity.IsDeleted.Should().BeFalse(); entity.DeletedAt.Should().BeNull(); entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); } [Fact] public void SoftDelete_ShouldSet_IsDeletedFlag() { var entity = new $firstE { Name = "Test" }; entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; entity.IsDeleted.Should().BeTrue(); entity.DeletedAt.Should().NotBeNull(); } } `````` --- ## Application.Tests — Handler Unit Test Handler'lari mock repository ile test eder. ``````csharp // tests/$ProjectName.Application.Tests/Features/${firstE}s/GetAll${firstE}sQueryHandlerTests.cs public class GetAll${firstE}sQueryHandlerTests { private readonly Mock<I${firstE}Repository> _repoMock = new(); private readonly Mock<IMapper> _mapperMock = new(); [Fact] public async Task Handle_ShouldReturn_MappedDtos_WhenEntitiesExist() { // Arrange var entities = new List<$firstE> { new() { Id = 1, Name = "Test" } }; var dtos = new List<${firstE}Dto> { new(1, "Test", DateTime.UtcNow) }; _repoMock.Setup(r => r.GetAllAsync()).ReturnsAsync(entities); _mapperMock.Setup(m => m.Map<IEnumerable<${firstE}Dto>>(entities)).Returns(dtos); var handler = new GetAll${firstE}sQueryHandler(_repoMock.Object, _mapperMock.Object); // Act var result = await handler.Handle(new GetAll${firstE}sQuery(), CancellationToken.None); // Assert result.Should().HaveCount(1); result.First().Id.Should().Be(1); _repoMock.Verify(r => r.GetAllAsync(), Times.Once); } [Fact] public async Task Handle_GetPaged_ShouldReturn_PagedResult() { // Arrange var entities = new List<$firstE> { new() { Id = 1, Name = "Test" } }; var pagedResult = new PagedResult<$firstE> { Items = entities, TotalCount = 1, Page = 1, Size = 20 }; _repoMock.Setup(r => r.GetPagedAsync(It.IsAny<PagedQuery>())).ReturnsAsync(pagedResult); _mapperMock.Setup(m => m.Map<IEnumerable<${firstE}Dto>>(entities)).Returns(new List<${firstE}Dto>()); var handler = new GetPaged${firstE}sQueryHandler(_repoMock.Object, _mapperMock.Object); // Act var result = await handler.Handle(new GetPaged${firstE}sQuery(1, 20), CancellationToken.None); // Assert result.TotalCount.Should().Be(1); result.Page.Should().Be(1); } } `````` --- ## Validation Tests ``````csharp public class Add${firstE}CommandValidatorTests { private readonly Add${firstE}CommandValidator _validator = new(); [Fact] public void Validate_ShouldFail_WhenNameIsEmpty() { var cmd = new Add${firstE}Command(""); var result = _validator.TestValidate(cmd); result.ShouldHaveValidationErrorFor(x => x.Name); } [Fact] public void Validate_ShouldPass_WhenNameIsValid() { var cmd = new Add${firstE}Command("Valid Name"); var result = _validator.TestValidate(cmd); result.ShouldNotHaveAnyValidationErrors(); } } `````` --- ## Integration.Tests — API Endpoint Test ``````csharp // tests/$ProjectName.Integration.Tests/${firstE}EndpointTests.cs public class ${firstE}EndpointTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; public ${firstE}EndpointTests(WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); } private async Task AuthenticateAsync() { var login = await _client.PostAsJsonAsync("/api/auth/login", new { username = "admin", password = "Admin123!" }); var body = await login.Content.ReadFromJsonAsync<JsonElement>(); var token = body.GetProperty("data").GetProperty("token").GetString(); _client.DefaultRequestHeaders.Authorization = new("Bearer", token); } [Fact] public async Task GetPaged_ShouldReturn200_WithPagedResult() { await AuthenticateAsync(); var response = await _client.GetAsync("/api/${firstLowE}s/paged?page=1&size=10"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync<JsonElement>(); body.GetProperty("success").GetBoolean().Should().BeTrue(); } [Fact] public async Task GetPaged_ShouldReturn401_WithoutToken() { var response = await _client.GetAsync("/api/${firstLowE}s/paged"); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } } `````` "@ } # ─── ci-cd.md ───────────────────────────────────────────── if (-not $SkipCi) { Write-Guide "ci-cd.md" @" # CI/CD Pipeline Kilavuzu Proje iki GitHub Actions workflow ile geliyor: - ``.github/workflows/ci.yml`` — Build + Test - ``.github/workflows/docker.yml`` — Docker image build + push --- ## ci.yml - Build ve Test Her ``push`` ve ``pull_request``'te tetiklenir: ``````yaml # Adimlar: # 1. dotnet restore # 2. dotnet build --no-restore # 3. dotnet test --no-build --verbosity normal `````` **Local test:** ``````bash dotnet restore dotnet build --no-restore dotnet test --no-build `````` --- ## docker.yml - Docker Build ``main`` branch'e push edildiginde tetiklenir: ``````yaml # Adimlar: # 1. Docker Buildx kur # 2. Docker Hub'a login (secrets gerekli) # 3. API image build + push `````` **Gerekli GitHub Secrets:** | Secret | Aciklama | |--------|----------| | ``DOCKER_USERNAME`` | Docker Hub kullanici adi | | ``DOCKER_PASSWORD`` | Docker Hub sifre/token | --- ## Local Docker Calistirma ``````bash # Tum stack (API + DB + UI) docker-compose up --build # Sadece API + DB docker-compose up api db # Loglar docker-compose logs -f api `````` --- $(if ($OrmMode -ne "Dapper") { @" --- ## Migration Yonetimi Projede hazir migration script'leri bulunur: ``````powershell # Windows - Yeni migration ekle .\scripts\migrate.ps1 -MigrationName "AddOrderTable" # Windows - Son migration'i geri al .\scripts\migrate.ps1 -Revert `````` ``````bash # Linux/Mac ./scripts/migrate.sh AddOrderTable ./scripts/migrate.sh --revert `````` "@ })--- ## Ortam Degiskenleri | Degisken | Kapsam | Aciklama | |----------|--------|----------| | ``ConnectionStrings__DefaultConnection`` | API | Veritabani baglanti dizesi | | ``JwtSettings__Secret`` | API | JWT imzalama anahtari (min 32 char) | | ``JwtSettings__ExpiryMinutes`` | API | Token gecerlilik suresi | | ``VITE_API_URL`` | Frontend | Backend API URL (Vite build-time) | | ``NEXT_PUBLIC_API_URL`` | Frontend | Backend API URL (Next.js build-time) | "@ } # ─── observability.md ───────────────────────────────────── if ($Observability) { Write-Guide "observability.md" @" # Observability Kilavuzu - Serilog + OpenTelemetry ## Serilog (Structured Logging) Tum loglar yapısal formatta yazilir. Gelistirme: console + Seq. Production: dosya veya cloud sink. ``````csharp // Handler icinde log kullanimi: _logger.LogInformation("${firstE} eklendi. Id={Id}, Name={Name}", entity.Id, entity.Name); _logger.LogWarning("${firstE} bulunamadi. Id={Id}", id); _logger.LogError(ex, "${firstE} eklenirken hata olustu."); `````` **Seq UI:** http://localhost:5341 (docker-compose.override.yml ile) --- ## OpenTelemetry (Distributed Tracing) Her HTTP istek otomatik trace edilir. Span'lar Jaeger/Zipkin/OTLP'ye gonderilebilir. ``````csharp // Ek span eklemek icin: using var activity = Activity.Current?.Source.StartActivity("CustomOperation"); activity?.SetTag("entity.id", id); activity?.SetTag("entity.type", "$firstE"); `````` --- ## Metrikler | Metrik | Aciklama | |--------|----------| | ``http.server.request.duration`` | HTTP istek suresi | | ``http.server.active_requests`` | Aktif istek sayisi | **Dashboard:** Grafana + Prometheus ile izlenebilir. "@ } # ─── frontend-guide.md ──────────────────────────────────── if (-not $SkipFrontend) { $isNextJs = ($FrontendMode -eq "NextJS") $isTS = ($FrontendMode -ne "ReactJS") $apiBaseNote = if ($isNextJs) { "``process.env.NEXT_PUBLIC_API_URL``" } else { "``import.meta.env.VITE_API_URL``" } $routerNote = if ($isNextJs) { "Next.js App Router (``app/`` klasoru). Her klasor bir route." } else { "React Router v6 (``BrowserRouter + Routes``). ``App.tsx``'de tanimli." } $i18nSection = if ($I18n) { @" ## i18n (Cok Dil Destegi) ``````typescript import { useTranslation } from 'react-i18next'; export function MyComponent() { const { t, i18n } = useTranslation(); return ( <div> <p>{t('app.brand')}</p> <button onClick={() => i18n.changeLanguage('tr')}>TR</button> <button onClick={() => i18n.changeLanguage('en')}>EN</button> </div> ); } `````` Hazir ``LanguageSwitcher`` bileseni ``components/common/LanguageSwitcher.tsx``'de uretilir: ``````typescript import { LanguageSwitcher } from '../components/common/LanguageSwitcher'; // Navbar veya App.tsx icine ekle: <LanguageSwitcher /> `````` Dil dosyalari: ``public/locales/en/common.json`` ve ``public/locales/tr/common.json`` Yeni anahtar eklemek icin her iki dosyaya da ekle: ``````json { "myKey": "My Value" } // en/common.json { "myKey": "Degerim" } // tr/common.json `````` "@ } else { "" } $webDir = if ($Platform -eq "Both") { "apps/web" } else { "frontend" } Write-Guide "frontend-guide.md" @" # Frontend Gelistirici Kilavuzu Framework: **$FrontendMode** --- ## Klasor Yapisi ``` $webDir/ +-- $(if ($isNextJs) { 'app/ <- Next.js App Router sayfalar' } else { 'pages/ <- Sayfa bileşenleri (LoginPage, {E}PageV2)' }) +-- components/ | +-- auth/ <- LoginForm | +-- common/ <- Button, Input, Alert | +-- {entity}s/ <- {Entity}Form, {Entity}Table +-- context/ | +-- AuthContext <- Kimlik dogrulama state +-- hooks/ | +-- use{Entity}s <- React Query hooks (liste + mutasyon) +-- services/ | +-- authService <- Login/logout/token yonetimi | +-- {e}Service <- API cagrisi (getAll, getPaged, add, remove) +-- types/ | +-- index.ts <- Interface tanimlari ``` --- ## Kimlik Dogrulama Akisi `````` 1. LoginPage -> authService.login({ username, password }) 2. API: POST /api/auth/login -> { success: true, data: { token, username } } 3. Token localStorage'a kaydedilir 4. AuthContext.login(token, username) -> isAuthenticated: true 5. PrivateRoute -> korunanli sayfalara erisim 6. 401 gelirse -> logout() + navigate('/login') `````` ``````typescript // AuthContext kullanimi: const { username, isAuthenticated, login, logout } = useAuth(); `````` --- ## React Query Kurulumu ``QueryClientProvider`` ``main.tsx``'e otomatik eklenir. Varsayilan konfigurasyon: ``````typescript const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1 } }, }); // main.tsx: <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> `````` --- ## React Query Hooks Her entity icin hazir hook'lar mevcuttur: ``````typescript // Sayfalı liste: const { data, isLoading, isError } = use${firstPE}(page, size); const items = data?.data?.items ?? []; const totalPages = data?.data?.totalPages ?? 1; const hasNext = data?.data?.hasNextPage ?? false; // Ekle (mutasyon): const { mutate: add${firstE} } = useAdd${firstE}(); add${firstE}({ name: 'Yeni', price: '99.99' }); // Sil: const { mutate: delete${firstE} } = useDelete${firstE}(); delete${firstE}(itemId); // Cache otomatik guncellenir (invalidateQueries) `````` --- ## API Servisi ``````typescript // services/${firstLowE}Service.ts ${firstLowE}Service.getAll() // Tum liste ${firstLowE}Service.getPaged(page, size) // Sayfalı liste ${firstLowE}Service.add(name, price) // Ekle ${firstLowE}Service.remove(id) // Sil (soft delete) `````` API URL: $apiBaseNote (ortam degiskeninden gelir) --- ## Router $routerNote $(if (-not $isNextJs) { @" `````` / -> /{firstLowE}s (redirect) /login -> LoginPage /{entity}s -> {Entity}PageV2 (PrivateRoute ile korunmus) `````` "@ }) --- ## Yeni Entity Sayfasi Ekleme 1. ``services/{entity}Service.ts`` ekle (getAll, getPaged, add, remove) 2. ``types/index.ts``'e interface ekle 3. ``hooks/use{Entity}s.ts`` ekle (useQuery + useMutation) 4. ``components/{entity}s/{Entity}Form.tsx`` ekle 5. ``components/{entity}s/{Entity}Table.tsx`` ekle 6. ``pages/{Entity}PageV2.tsx`` ekle 7. ``App.tsx``'de route tanimla --- ## Ortam Degiskenleri `````` .env <- Gelistirme (http://localhost:8080/api) .env.docker <- Docker (http://api:8080/api) `````` --- ## Build ``````bash npm run dev # Gelistirme sunucusu (HMR) npm run build # Production build (tsc + vite build / next build) npm run preview # Build onizleme (sadece Vite) `````` $i18nSection "@ } if ($Platform -match "Mobile|Both") { $sharedImportNote = if ($Platform -eq "Both") { "import { ApiClient } from 'shared';" } else { "import { ApiClient } from '../shared';" } Write-Guide "mobile-guide.md" @" # Mobil Gelistirici Kilavuzu (React Native / Expo) Uygulama, **Expo Managed Workflow** ve **Expo Router** kullanılarak geliştirilmiştir. --- ## Klasör Yapısı ``` mobile/ (veya apps/mobile/) +-- app/ | +-- (auth)/login.tsx <- Giriş ekranı | +-- (tabs)/ | +-- _layout.tsx <- Sekme yönlendirmeleri | +-- index.tsx <- Ana Dashboard | +-- {entity}s/ <- Entity listeleme ve ekleme ekranları | +-- _layout.tsx <- Kök sağlayıcılar (React Query, AuthProvider) +-- context/ | +-- AuthContext.tsx <- expo-secure-store tabanlı JWT yönetimi +-- hooks/ | +-- use{Entity}s.ts <- React Query hooks +-- services/ | +-- api.ts <- Ortak ApiClient örneği ``` --- ## Ortak Paket Kullanımı Web ve Mobil arasındaki kod paylaşımı `shared` paketi üzerinden yönetilir: ```typescript $sharedImportNote // ApiClient doğrudan enjekte edilen base URL ile çalışır: export const api = new ApiClient(process.env.EXPO_PUBLIC_API_URL); ``` --- ## Geliştirme ve Çalıştırma Uygulamayı yerel simülatörde veya **Expo Go** uygulaması ile kendi fiziksel cihazınızda çalıştırmak için: ```bash # Bağımlılıkları kurun (kök dizinde) npm install # Expo Metro bundler sunucusunu başlatın npm start ``` Metro sunucusu başladıktan sonra terminalde çıkan **QR kodu** telefonunuzun kamerası (iOS) veya Expo Go uygulaması (Android) ile taratarak uygulamayı anında cihazınızda test edebilirsiniz. "@ } |