Updating structure
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260925-234832-1pc0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Rename all projects in the repository from single-word names (`Build`, `Deploy`, `Model`, `Web`) to qualified names starting with the `ZeroSpace.` prefix (`ZeroSpace.Build`, `ZeroSpace.Deploy`, `ZeroSpace.Model`, `ZeroSpace.Web`). Ensure all solution definitions, project files, folder paths, project references, namespaces, and script paths are updated consistently.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope:**
|
||||||
|
- Renaming project directories: `ZS/Build` → `ZS/ZeroSpace.Build`, `ZS/Deploy` → `ZS/ZeroSpace.Deploy`, `ZS/Model` → `ZS/ZeroSpace.Model`, `ZS/Web` → `ZS/ZeroSpace.Web`.
|
||||||
|
- Renaming `.csproj` files: `Build.csproj` → `ZeroSpace.Build.csproj`, `Deploy.csproj` → `ZeroSpace.Deploy.csproj`, `Model.csproj` → `ZeroSpace.Model.csproj`, `Web.csproj` → `ZeroSpace.Web.csproj`.
|
||||||
|
- Updating `ZS/ZeroSpace.sln` project declarations and configuration mappings.
|
||||||
|
- Updating `<ProjectReference>` in `ZeroSpace.Web.csproj`.
|
||||||
|
- Updating C# namespaces and `@using` directives to match the `ZeroSpace.*` convention.
|
||||||
|
- Updating hardcoded project/folder paths in `ZeroSpace.Build/Program.cs` and `ZeroSpace.Deploy/Program.cs`.
|
||||||
|
- **Out of Scope:**
|
||||||
|
- Modifying business logic or UI behavior in the Blazor web application.
|
||||||
|
- Renaming Markdown documentation files under `zs.docs`.
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
The solution contains 4 projects in `ZS/`:
|
||||||
|
- `ZS/Model/Model.csproj`: Class library declaring `namespace Model;`.
|
||||||
|
- `ZS/Build/Build.csproj`: Console generator referencing `Model/Units.g.cs` and scanning for `ZS.sln`.
|
||||||
|
- `ZS/Deploy/Deploy.csproj`: Deployment tool targeting `../Web/Web.csproj`.
|
||||||
|
- `ZS/Web/Web.csproj`: Blazor WebAssembly app referencing `..\Model\Model.csproj` and using `Web`, `Web.Layout`, and `Model`.
|
||||||
|
- `ZS/ZeroSpace.sln`: Solution listing `Web`, `Build`, `Model`, and `Deploy`.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
- **Rename Project Directories:** Project folder names will be updated to match the project names (`ZeroSpace.Build`, `ZeroSpace.Deploy`, `ZeroSpace.Model`, `ZeroSpace.Web`) to maintain standard .NET convention where directory name matches the assembly/project name.
|
||||||
|
- **Update Namespaces:** Align namespaces (`ZeroSpace.Model`, `ZeroSpace.Web`) with project naming for consistency across the solution.
|
||||||
|
- **Fix Solution Search in Build Tool:** Update `FindSolutionRoot` in `ZeroSpace.Build/Program.cs` to locate `ZeroSpace.sln` (or any `*.sln`).
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
- **Project Structure & Naming:**
|
||||||
|
- `ZS/Build/` → `ZS/ZeroSpace.Build/`
|
||||||
|
- `ZS/Build/Build.csproj` → `ZS/ZeroSpace.Build/ZeroSpace.Build.csproj`
|
||||||
|
- `ZS/Deploy/` → `ZS/ZeroSpace.Deploy/`
|
||||||
|
- `ZS/Deploy/Deploy.csproj` → `ZS/ZeroSpace.Deploy/ZeroSpace.Deploy.csproj`
|
||||||
|
- `ZS/Model/` → `ZS/ZeroSpace.Model/`
|
||||||
|
- `ZS/Model/Model.csproj` → `ZS/ZeroSpace.Model/ZeroSpace.Model.csproj`
|
||||||
|
- `ZS/Web/` → `ZS/ZeroSpace.Web/`
|
||||||
|
- `ZS/Web/Web.csproj` → `ZS/ZeroSpace.Web/ZeroSpace.Web.csproj`
|
||||||
|
- **Solution File (`ZS/ZeroSpace.sln`):**
|
||||||
|
- Update project name strings and project file paths to `ZeroSpace.*`.
|
||||||
|
- **Project Reference (`ZS/ZeroSpace.Web/ZeroSpace.Web.csproj`):**
|
||||||
|
- Change `..\Model\Model.csproj` to `..\ZeroSpace.Model\ZeroSpace.Model.csproj`.
|
||||||
|
- **Source Files & Namespaces:**
|
||||||
|
- `ZS/ZeroSpace.Model/UnitData.cs`: `namespace ZeroSpace.Model;`
|
||||||
|
- `ZS/ZeroSpace.Model/UnitEfficiency.cs`: `namespace ZeroSpace.Model;`
|
||||||
|
- `ZS/ZeroSpace.Model/Units.g.cs`: `namespace ZeroSpace.Model;`
|
||||||
|
- `ZS/ZeroSpace.Build/Program.cs`: update model path to `ZeroSpace.Model`, generate `namespace ZeroSpace.Model;`, check `ZeroSpace.sln`.
|
||||||
|
- `ZS/ZeroSpace.Deploy/Program.cs`: update path lookups for `ZeroSpace.Web` and `ZeroSpace.Web.csproj`.
|
||||||
|
- `ZS/ZeroSpace.Web/Program.cs`: `using ZeroSpace.Web;`
|
||||||
|
- `ZS/ZeroSpace.Web/_Imports.razor`: `@using ZeroSpace.Web`, `@using ZeroSpace.Web.Layout`, `@using ZeroSpace.Model`
|
||||||
|
- `ZS/ZeroSpace.Web/Pages/Units.razor`: `@using ZeroSpace.Model`
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Sln[ZS/ZeroSpace.sln] --> Web[ZS/ZeroSpace.Web]
|
||||||
|
Sln --> Model[ZS/ZeroSpace.Model]
|
||||||
|
Sln --> Build[ZS/ZeroSpace.Build]
|
||||||
|
Sln --> Deploy[ZS/ZeroSpace.Deploy]
|
||||||
|
Web --> Model
|
||||||
|
Build -. generates Units.g.cs .-> Model
|
||||||
|
Deploy -. publishes .-> Web
|
||||||
|
```
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- Verify solution builds cleanly via CLI (`dotnet build ZS/ZeroSpace.sln`).
|
||||||
|
- Run code generation via `dotnet run --project ZS/ZeroSpace.Build/ZeroSpace.Build.csproj` to confirm `Units.g.cs` is generated with the proper namespace.
|
||||||
|
- Validate project references and dependencies resolve without warnings or missing assembly errors.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
- `dotnet build ZS/ZeroSpace.sln` succeeds with 0 errors and 0 warnings.
|
||||||
|
- `ZeroSpace.Web` compiles with reference to `ZeroSpace.Model`.
|
||||||
|
- `ZeroSpace.Build` locates `zs.docs` and writes generated code into `ZS/ZeroSpace.Model/Units.g.cs`.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- Relative path traversal from build outputs (`AppContext.BaseDirectory` in `ZeroSpace.Deploy`) resolving the renamed `ZeroSpace.Web` project directory.
|
||||||
|
- `FindSolutionRoot` helper locating `ZeroSpace.sln` accurately.
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### ✓ Step 1: Rename project directories and project files
|
||||||
|
Project folders and `.csproj` files follow the `ZeroSpace.*` naming convention.
|
||||||
|
|
||||||
|
- Rename directory `ZS/Model` to `ZS/ZeroSpace.Model` and project file `Model.csproj` to `ZeroSpace.Model.csproj`.
|
||||||
|
- Rename directory `ZS/Build` to `ZS/ZeroSpace.Build` and project file `Build.csproj` to `ZeroSpace.Build.csproj`.
|
||||||
|
- Rename directory `ZS/Deploy` to `ZS/ZeroSpace.Deploy` and project file `Deploy.csproj` to `ZeroSpace.Deploy.csproj`.
|
||||||
|
- Rename directory `ZS/Web` to `ZS/ZeroSpace.Web` and project file `Web.csproj` to `ZeroSpace.Web.csproj`.
|
||||||
|
|
||||||
|
### ✓ Step 2: Update solution file and project references
|
||||||
|
`ZeroSpace.sln` and project reference paths correctly reference the renamed projects and directories.
|
||||||
|
|
||||||
|
- Update `ZS/ZeroSpace.sln` project entries and relative paths for `ZeroSpace.Web`, `ZeroSpace.Build`, `ZeroSpace.Model`, and `ZeroSpace.Deploy`.
|
||||||
|
- Update project reference `<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />` in `ZS/ZeroSpace.Web/ZeroSpace.Web.csproj`.
|
||||||
|
|
||||||
|
### ✓ Step 3: Update namespaces and code path references
|
||||||
|
Source code files, generator scripts, and deployment scripts use the updated `ZeroSpace.*` namespaces and folder paths.
|
||||||
|
|
||||||
|
- Update C# namespaces in `ZS/ZeroSpace.Model/UnitData.cs`, `ZS/ZeroSpace.Model/UnitEfficiency.cs`, and `ZS/ZeroSpace.Model/Units.g.cs` to `ZeroSpace.Model`.
|
||||||
|
- Update code generator in `ZS/ZeroSpace.Build/Program.cs` to target `ZeroSpace.Model` folder, output `namespace ZeroSpace.Model;`, and verify solution root resolution for `ZeroSpace.sln`.
|
||||||
|
- Update deploy script paths in `ZS/ZeroSpace.Deploy/Program.cs` to locate `ZeroSpace.Web` and `ZeroSpace.Web.csproj`.
|
||||||
|
- Update `ZS/ZeroSpace.Web/Program.cs`, `ZS/ZeroSpace.Web/_Imports.razor`, and `ZS/ZeroSpace.Web/Pages/Units.razor` to reference `ZeroSpace.Web`, `ZeroSpace.Web.Layout`, and `ZeroSpace.Model`.
|
||||||
|
|
||||||
|
### ✓ Step 4: Validate solution build and code generation
|
||||||
|
The entire solution builds cleanly with zero errors/warnings and code generation runs successfully.
|
||||||
|
|
||||||
|
- Run `dotnet build ZS/ZeroSpace.sln` to ensure all projects compile and link without errors.
|
||||||
|
- Run `ZeroSpace.Build` console application to ensure unit code generation completes and updates `ZeroSpace.Model/Units.g.cs` correctly.
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
using Web;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
||||||
builder.RootComponents.Add<App>("#app");
|
|
||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
|
||||||
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
|
||||||
|
|
||||||
builder.Services.AddTelerikBlazor();
|
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{16296B27-D17B-4BFF-B08F-60F20DF36066}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Build\Build.csproj", "{C1C04020-AFD6-493B-ABDB-277977D3488E}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{E31A06B0-9B2B-4805-B343-5687A5992E9A}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deploy", "Deploy\Deploy.csproj", "{DECE54F2-51BB-4D7D-BD20-BE7F1C298FA3}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{16296B27-D17B-4BFF-B08F-60F20DF36066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{16296B27-D17B-4BFF-B08F-60F20DF36066}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{16296B27-D17B-4BFF-B08F-60F20DF36066}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{16296B27-D17B-4BFF-B08F-60F20DF36066}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{C1C04020-AFD6-493B-ABDB-277977D3488E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C1C04020-AFD6-493B-ABDB-277977D3488E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C1C04020-AFD6-493B-ABDB-277977D3488E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C1C04020-AFD6-493B-ABDB-277977D3488E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E31A06B0-9B2B-4805-B343-5687A5992E9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{E31A06B0-9B2B-4805-B343-5687A5992E9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{E31A06B0-9B2B-4805-B343-5687A5992E9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E31A06B0-9B2B-4805-B343-5687A5992E9A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{DECE54F2-51BB-4D7D-BD20-BE7F1C298FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{DECE54F2-51BB-4D7D-BD20-BE7F1C298FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{DECE54F2-51BB-4D7D-BD20-BE7F1C298FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{DECE54F2-51BB-4D7D-BD20-BE7F1C298FA3}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
**/.git
|
||||||
|
**/.vs
|
||||||
|
**/.idea
|
||||||
|
**/.junie
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/node_modules
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260925-235814-7y2w
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
The goal is to modernize the ZeroSpace application by introducing **.NET Aspire** orchestration, adding a dedicated backend **GraphQL API service** powered by **Hot Chocolate** with **Nitro UI (Banana Cake Pop)**, integrating a **PostgreSQL database**, containerizing components with **Docker**, and managing deployments on **Kubernetes** using industry-standard tooling (Aspirate / K8s manifests).
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope**:
|
||||||
|
- Creation of `.NET Aspire` AppHost (`ZeroSpace.AppHost`) and shared Service Defaults (`ZeroSpace.ServiceDefaults`).
|
||||||
|
- Creation of an ASP.NET Core GraphQL API service (`ZeroSpace.ApiService`) using **Hot Chocolate** with Entity Framework Core & PostgreSQL (`Npgsql`) support.
|
||||||
|
- Interactive **Nitro UI** (Banana Cake Pop) GraphQL IDE integration at the `/graphql` endpoint for rapid schema exploration and testing.
|
||||||
|
- GraphQL queries, mutations, filtering, sorting, and projections for `UnitData`.
|
||||||
|
- PostgreSQL container provisioning and database connection orchestration via Aspire.
|
||||||
|
- Updating `ZeroSpace.Web` (Blazor WASM) to consume GraphQL operations from `ZeroSpace.ApiService`.
|
||||||
|
- Docker containerization for both `ZeroSpace.ApiService` and `ZeroSpace.Web`.
|
||||||
|
- Kubernetes manifest management and Aspirate integration for cluster deployment.
|
||||||
|
- **Out of Scope**:
|
||||||
|
- Modifying the unit model generation logic in `ZeroSpace.Build`.
|
||||||
|
- Cloud-specific managed Kubernetes cluster creation scripts (e.g. Terraform/Bicep for AKS/EKS).
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- **As a Developer**, I want to start the web app, GraphQL API, and PostgreSQL database with a single command using .NET Aspire, and use Nitro UI to interactively explore and query schemas so that local development and testing maximize productivity.
|
||||||
|
- **As an Operator**, I want to generate Kubernetes deployment manifests from the Aspire architecture so that all services and databases deploy consistently to production Kubernetes clusters.
|
||||||
|
- **As a User**, I want to browse and manage ZeroSpace unit data through the Blazor Web UI backed by efficient, precision GraphQL data fetching and persistent database storage.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- **FR-1**: The Aspire AppHost must orchestrate the PostgreSQL container, GraphQL API service, and Web app with configured dependencies and environment variables.
|
||||||
|
- **FR-2**: The `ZeroSpace.ApiService` must expose a Hot Chocolate GraphQL server with schema filtering, sorting, and projection capabilities over PostgreSQL `UnitData`.
|
||||||
|
- **FR-3**: The Nitro UI GraphQL IDE must be hosted and accessible at `/graphql` to enable in-browser schema exploration, query execution, and debugging.
|
||||||
|
- **FR-4**: Database schema migrations and initial seed data from `ZeroSpace.Model.Units.All` must be executed automatically upon startup.
|
||||||
|
- **FR-5**: `ZeroSpace.Web` must execute GraphQL queries/mutations against `ZeroSpace.ApiService` using configured HTTP endpoints.
|
||||||
|
- **FR-6**: Container definitions (Dockerfiles) must exist for all custom applications.
|
||||||
|
- **FR-7**: Kubernetes manifests must specify Deployments, Services, PersistentVolumeClaims, Secrets, and Ingress routing for Web and GraphQL endpoints.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- **Observability**: OpenTelemetry tracing, metrics, and structured logging enabled across services via `ServiceDefaults` (including Hot Chocolate diagnostic tracing).
|
||||||
|
- **Resilience & Health**: Health check endpoints (`/health`, `/alive`) configured for Kubernetes readiness and liveness probes.
|
||||||
|
- **Security**: Database credentials managed through environment variables, Secrets, and Aspire connection strings without hardcoded passwords.
|
||||||
|
- **Query Efficiency**: GraphQL projections ensure only requested fields are fetched from PostgreSQL, minimizing network transfer and database payload overhead.
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- `ZeroSpace.Web` is a standalone Blazor WebAssembly application (`Microsoft.NET.Sdk.BlazorWebAssembly`) targeting .NET 10 with Telerik UI for Blazor.
|
||||||
|
- `ZeroSpace.Model` contains records like `UnitData.cs`, `UnitEfficiency.cs`, and generated static unit arrays in `Units.g.cs`.
|
||||||
|
- `ZeroSpace.Deploy` publishes static files to Azure Static Web Apps.
|
||||||
|
- `ZeroSpace.AppHost`, `ZeroSpace.ServiceDefaults`, and `ZeroSpace.ApiService` are integrated into the solution.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
1. **Hot Chocolate GraphQL Server with Nitro UI (`ZeroSpace.ApiService`)**:
|
||||||
|
- *Decision*: Introduce Hot Chocolate GraphQL server (`HotChocolate.AspNetCore`, `HotChocolate.Data.EntityFramework`) and enable the embedded Nitro UI (Banana Cake Pop) GraphQL tool suite.
|
||||||
|
- *Rationale*: GraphQL enables client-driven data fetching, reducing over-fetching and network serialization overhead. Nitro UI provides a rich developer and operator environment for real-time schema introspection, query building, and mutation execution directly in the browser.
|
||||||
|
2. **.NET Aspire Orchestration & Aspirate for Kubernetes**:
|
||||||
|
- *Decision*: Use .NET Aspire AppHost for local orchestration and developer dashboard, and use Aspirate (Aspir8) alongside standard Kubernetes manifests.
|
||||||
|
- *Rationale*: Aspire provides official .NET cloud-native developer tooling, OpenTelemetry defaults, and service discovery, while Aspirate transforms the Aspire manifest into production-ready Kubernetes Helm/Kustomize/YAML artifacts.
|
||||||
|
3. **Database Architecture & Seeding**:
|
||||||
|
- *Decision*: Entity Framework Core with `Npgsql.EntityFrameworkCore.PostgreSQL` and PostgreSQL 17 container.
|
||||||
|
- *Rationale*: Native EF Core support with Aspire component `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` enables automatic connection string binding, resilient retries, and database health checks.
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph Local / Dev [Local .NET Aspire AppHost]
|
||||||
|
AppHost[ZeroSpace.AppHost]
|
||||||
|
Dash[Aspire Dashboard]
|
||||||
|
Nitro[Nitro UI / Banana Cake Pop]
|
||||||
|
AppHost --> Dash
|
||||||
|
AppHost --> Nitro
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Kubernetes Cluster [Kubernetes Cluster]
|
||||||
|
Ingress[Kubernetes Ingress / Gateway]
|
||||||
|
|
||||||
|
subgraph Web Pod [Web Deployment]
|
||||||
|
Web[ZeroSpace.Web / Nginx]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph API Pod [API Deployment]
|
||||||
|
API[ZeroSpace.ApiService / Hot Chocolate]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DB Pod [PostgreSQL StatefulSet / Pod]
|
||||||
|
DB[(PostgreSQL 17)]
|
||||||
|
PVC[PersistentVolumeClaim]
|
||||||
|
DB --- PVC
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Ingress --> Web
|
||||||
|
Ingress --> API
|
||||||
|
Web -->|GraphQL Queries / Mutations| API
|
||||||
|
Nitro -->|Interactive Queries / Schema Docs| API
|
||||||
|
API -->|EF Core / Npgsql| DB
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- **`ZeroSpace.AppHost`**: Aspire orchestrator project that links PostgreSQL container, `ZeroSpace.ApiService`, and `ZeroSpace.Web`.
|
||||||
|
- **`ZeroSpace.ServiceDefaults`**: Shared project configuring OpenTelemetry, health checks, discovery, and resilience policies.
|
||||||
|
- **`ZeroSpace.ApiService`**: ASP.NET Core GraphQL API powered by Hot Chocolate, hosting Nitro UI at `/graphql`, and handling PostgreSQL persistence with EF Core.
|
||||||
|
- **`ZeroSpace.Web`**: Client-side Blazor WASM UI configured with `IUnitsApiClient` communicating with `ZeroSpace.ApiService` via GraphQL.
|
||||||
|
- **`k8s/`**: Kubernetes manifests (Deployments, Services, PVC, Ingress, Secrets) and Aspirate generation configuration.
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
ZeroSpace/
|
||||||
|
├── ZeroSpace.AppHost/
|
||||||
|
│ ├── Program.cs
|
||||||
|
│ ├── appsettings.json
|
||||||
|
│ └── ZeroSpace.AppHost.csproj
|
||||||
|
├── ZeroSpace.ServiceDefaults/
|
||||||
|
│ ├── Extensions.cs
|
||||||
|
│ └── ZeroSpace.ServiceDefaults.csproj
|
||||||
|
├── ZeroSpace.ApiService/
|
||||||
|
│ ├── Data/
|
||||||
|
│ │ ├── ZeroSpaceDbContext.cs
|
||||||
|
│ │ └── DbInitializer.cs
|
||||||
|
│ ├── GraphQL/
|
||||||
|
│ │ ├── Query.cs
|
||||||
|
│ │ ├── Mutation.cs
|
||||||
|
│ │ └── Types/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── Program.cs
|
||||||
|
│ └── ZeroSpace.ApiService.csproj
|
||||||
|
├── ZeroSpace.Web/
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── IUnitsApiClient.cs
|
||||||
|
│ │ └── UnitsApiClient.cs
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── nginx.conf
|
||||||
|
│ └── ...
|
||||||
|
├── k8s/
|
||||||
|
│ ├── postgres-pvc.yaml
|
||||||
|
│ ├── postgres-deployment.yaml
|
||||||
|
│ ├── apiservice-deployment.yaml
|
||||||
|
│ ├── web-deployment.yaml
|
||||||
|
│ └── ingress.yaml
|
||||||
|
├── aspirate.json
|
||||||
|
└── ZeroSpace.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Models / Contracts
|
||||||
|
```graphql
|
||||||
|
|
||||||
|
# GraphQL Schema Contract
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
units(
|
||||||
|
where: UnitDataFilterInput
|
||||||
|
order: [UnitDataSortInput!]
|
||||||
|
): [UnitData!]!
|
||||||
|
|
||||||
|
unitById(id: String!): UnitData
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUnit(input: CreateUnitInput!): UnitData!
|
||||||
|
updateUnit(id: String!, input: UpdateUnitInput!): UnitData!
|
||||||
|
deleteUnit(id: String!): Boolean!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks & Mitigations
|
||||||
|
- **Risk**: GraphQL N+1 or deep query performance impacts.
|
||||||
|
- *Mitigation*: Leverage Hot Chocolate's `[UseProjection]`, `[UseFiltering]`, and `[UseSorting]` which translate GraphQL AST directly into optimized PostgreSQL SQL via EF Core `IQueryable`.
|
||||||
|
- **Risk**: Nitro UI access across different deployment environments.
|
||||||
|
- *Mitigation*: Configure Nitro UI / Banana Cake Pop middleware settings so developer tooling is available locally and behind authorized Ingress routes in staging/production as needed.
|
||||||
|
- **Risk**: Blazor WASM CORS issues when executing GraphQL POST requests.
|
||||||
|
- *Mitigation*: Configure permissive CORS policies in `ZeroSpace.ApiService` for GraphQL endpoints and route requests through Kubernetes Ingress.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- Verify local orchestration via `dotnet run --project ZeroSpace.AppHost`.
|
||||||
|
- Access Nitro UI at `http://localhost:5142/graphql` (or through Aspire Dashboard endpoint) to test interactive queries, mutations, and schema documentation.
|
||||||
|
- Test GraphQL queries and mutations executed by `ZeroSpace.Web` against `ZeroSpace.ApiService`.
|
||||||
|
- Build and run Docker containers for `ZeroSpace.ApiService` and `ZeroSpace.Web`.
|
||||||
|
- Validate Kubernetes manifest syntax and Aspirate manifest compilation (`aspirate generate`).
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
1. **Aspire Local Launch**: Starting `ZeroSpace.AppHost` boots PostgreSQL container, runs EF Core migrations, starts `ZeroSpace.ApiService` with Hot Chocolate GraphQL & Nitro UI, and launches `ZeroSpace.Web`.
|
||||||
|
2. **Nitro UI Interactive Execution**: Open `/graphql` in browser; Banana Cake Pop / Nitro UI loads; execute `{ units { id name hexite flux tier faction } }` and receive structured JSON response.
|
||||||
|
3. **Blazor UI Data Flow**: Navigating to `/units` in the Blazor UI sends a GraphQL query payload to `ZeroSpace.ApiService`, which queries PostgreSQL and returns unit records.
|
||||||
|
4. **Container Build**: `docker build` succeeds for both `ZeroSpace.ApiService` and `ZeroSpace.Web` Dockerfiles.
|
||||||
|
5. **Kubernetes Deployment**: Applying the generated Kubernetes manifests provisions the PVC, deploys PostgreSQL, starts API/Web pods, and exposes them via Service and Ingress.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- **PostgreSQL Connection Failures**: GraphQL queries gracefully return structured GraphQL errors and health checks report unhealthy status.
|
||||||
|
- **Empty Database**: Seeding logic populates initial units from `ZeroSpace.Model` if the database table is empty before serving GraphQL requests.
|
||||||
|
- **Complex GraphQL Filtering**: Verify filtering by tier and faction compiles to efficient SQL `WHERE` clauses in PostgreSQL without in-memory buffering.
|
||||||
|
|
||||||
|
# Execution Plan
|
||||||
|
|
||||||
|
### ✓ Step 1: Configure Hot Chocolate GraphQL and Nitro UI in ZeroSpace.ApiService
|
||||||
|
### ✓ Step 2: Update ZeroSpace.Web to consume GraphQL API operations
|
||||||
|
### ✓ Step 3: Update AppHost, Ingress, and Kubernetes configuration for GraphQL endpoint
|
||||||
|
### ✓ Step 4: Build and Verify Solution
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.ApiService.Data;
|
||||||
|
|
||||||
|
public static class DbInitializer
|
||||||
|
{
|
||||||
|
public static async Task InitializeDatabaseAsync(IServiceProvider serviceProvider, ILogger logger, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<ZeroSpaceDbContext>();
|
||||||
|
|
||||||
|
logger.LogInformation("Ensuring database schema exists...");
|
||||||
|
await db.Database.EnsureCreatedAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await db.Units.AnyAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
logger.LogInformation("Seeding initial units into PostgreSQL...");
|
||||||
|
var initialUnits = Units.All;
|
||||||
|
await db.Units.AddRangeAsync(initialUnits, cancellationToken);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
logger.LogInformation("Seeded {Count} units into PostgreSQL database.", initialUnits.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("Database already contains unit data. Skipping seed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.ApiService.Data;
|
||||||
|
|
||||||
|
public class ZeroSpaceDbContext : DbContext
|
||||||
|
{
|
||||||
|
public ZeroSpaceDbContext(DbContextOptions<ZeroSpaceDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<UnitData> Units => Set<UnitData>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<UnitData>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("units");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
|
||||||
|
entity.Property(e => e.Id).HasColumnName("id").HasMaxLength(100);
|
||||||
|
entity.Property(e => e.Name).HasColumnName("name").IsRequired().HasMaxLength(200);
|
||||||
|
entity.Property(e => e.Hexite).HasColumnName("hexite");
|
||||||
|
entity.Property(e => e.Flux).HasColumnName("flux");
|
||||||
|
entity.Property(e => e.Supply).HasColumnName("supply");
|
||||||
|
entity.Property(e => e.ProductionTime).HasColumnName("production_time");
|
||||||
|
entity.Property(e => e.Health).HasColumnName("health");
|
||||||
|
entity.Property(e => e.Energy).HasColumnName("energy");
|
||||||
|
entity.Property(e => e.Shields).HasColumnName("shields");
|
||||||
|
entity.Property(e => e.ArmorRating).HasColumnName("armor_rating");
|
||||||
|
entity.Property(e => e.MovementSpeed).HasColumnName("movement_speed");
|
||||||
|
entity.Property(e => e.DamagePerSecond).HasColumnName("damage_per_second");
|
||||||
|
entity.Property(e => e.AttackRange).HasColumnName("attack_range");
|
||||||
|
entity.Property(e => e.Tier).HasColumnName("tier");
|
||||||
|
entity.Property(e => e.Faction).HasColumnName("faction").IsRequired().HasMaxLength(100);
|
||||||
|
entity.Property(e => e.Hotkey).HasColumnName("hotkey").HasMaxLength(20);
|
||||||
|
entity.Property(e => e.BuildAtSameTime).HasColumnName("build_at_same_time");
|
||||||
|
entity.Property(e => e.Limit).HasColumnName("limit");
|
||||||
|
|
||||||
|
var valueComparer = new ValueComparer<IReadOnlyList<string>>(
|
||||||
|
(c1, c2) => (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.SequenceEqual(c2)),
|
||||||
|
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||||
|
c => c.ToList()
|
||||||
|
);
|
||||||
|
|
||||||
|
entity.Property(e => e.Attributes)
|
||||||
|
.HasColumnName("attributes")
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
|
||||||
|
v => (IReadOnlyList<string>)(JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>())
|
||||||
|
)
|
||||||
|
.Metadata.SetValueComparer(valueComparer);
|
||||||
|
|
||||||
|
entity.Ignore(e => e.DpsPerHexite);
|
||||||
|
entity.Ignore(e => e.DpsPerFlux);
|
||||||
|
entity.Ignore(e => e.DpsPerTotalCost);
|
||||||
|
entity.Ignore(e => e.HealthPerHexite);
|
||||||
|
entity.Ignore(e => e.HealthPerFlux);
|
||||||
|
entity.Ignore(e => e.HealthPerTotalCost);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy project files for caching restore layer
|
||||||
|
COPY ["ZeroSpace.ApiService/ZeroSpace.ApiService.csproj", "ZeroSpace.ApiService/"]
|
||||||
|
COPY ["ZeroSpace.Model/ZeroSpace.Model.csproj", "ZeroSpace.Model/"]
|
||||||
|
COPY ["ZeroSpace.ServiceDefaults/ZeroSpace.ServiceDefaults.csproj", "ZeroSpace.ServiceDefaults/"]
|
||||||
|
|
||||||
|
RUN dotnet restore "ZeroSpace.ApiService/ZeroSpace.ApiService.csproj"
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/ZeroSpace.ApiService"
|
||||||
|
RUN dotnet publish "ZeroSpace.ApiService.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 8443
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "ZeroSpace.ApiService.dll"]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ZeroSpace.ApiService.Data;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.ApiService.Endpoints;
|
||||||
|
|
||||||
|
public static class UnitsEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapUnitsEndpoints(this IEndpointRouteBuilder routes)
|
||||||
|
{
|
||||||
|
var group = routes.MapGroup("/api/units")
|
||||||
|
.WithTags("Units");
|
||||||
|
|
||||||
|
group.MapGet("/", async (ZeroSpaceDbContext db, string? faction, int? tier) =>
|
||||||
|
{
|
||||||
|
IQueryable<UnitData> query = db.Units.AsNoTracking();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction))
|
||||||
|
{
|
||||||
|
query = query.Where(u => u.Faction == faction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(u => u.Tier == tier.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var units = await query.ToListAsync();
|
||||||
|
return Results.Ok(units);
|
||||||
|
})
|
||||||
|
.WithName("GetAllUnits")
|
||||||
|
.WithSummary("Get all units optionally filtered by faction and tier.");
|
||||||
|
|
||||||
|
group.MapGet("/{id}", async (string id, ZeroSpaceDbContext db) =>
|
||||||
|
{
|
||||||
|
var unit = await db.Units.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
|
||||||
|
return unit is not null ? Results.Ok(unit) : Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithName("GetUnitById")
|
||||||
|
.WithSummary("Get a unit by id.");
|
||||||
|
|
||||||
|
group.MapPost("/", async (UnitData unit, ZeroSpaceDbContext db) =>
|
||||||
|
{
|
||||||
|
var exists = await db.Units.AnyAsync(u => u.Id == unit.Id);
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
return Results.Conflict($"Unit with id '{unit.Id}' already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Units.Add(unit);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Results.Created($"/api/units/{unit.Id}", unit);
|
||||||
|
})
|
||||||
|
.WithName("CreateUnit")
|
||||||
|
.WithSummary("Create a new unit.");
|
||||||
|
|
||||||
|
group.MapPut("/{id}", async (string id, UnitData updatedUnit, ZeroSpaceDbContext db) =>
|
||||||
|
{
|
||||||
|
var unit = await db.Units.FindAsync(id);
|
||||||
|
if (unit is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Units.Remove(unit);
|
||||||
|
db.Units.Add(updatedUnit with { Id = id });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Results.Ok(updatedUnit with { Id = id });
|
||||||
|
})
|
||||||
|
.WithName("UpdateUnit")
|
||||||
|
.WithSummary("Update an existing unit.");
|
||||||
|
|
||||||
|
group.MapDelete("/{id}", async (string id, ZeroSpaceDbContext db) =>
|
||||||
|
{
|
||||||
|
var unit = await db.Units.FindAsync(id);
|
||||||
|
if (unit is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Units.Remove(unit);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Results.NoContent();
|
||||||
|
})
|
||||||
|
.WithName("DeleteUnit")
|
||||||
|
.WithSummary("Delete a unit by id.");
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using HotChocolate;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ZeroSpace.ApiService.Data;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.ApiService.GraphQL;
|
||||||
|
|
||||||
|
public record CreateUnitInput(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
int Hexite,
|
||||||
|
int Flux,
|
||||||
|
int Supply,
|
||||||
|
int ProductionTime,
|
||||||
|
int Health,
|
||||||
|
int Energy,
|
||||||
|
int Shields,
|
||||||
|
int ArmorRating,
|
||||||
|
int MovementSpeed,
|
||||||
|
int DamagePerSecond,
|
||||||
|
int AttackRange,
|
||||||
|
IReadOnlyList<string>? Attributes,
|
||||||
|
int Tier,
|
||||||
|
string Faction,
|
||||||
|
string? Hotkey,
|
||||||
|
int BuildAtSameTime,
|
||||||
|
int? Limit
|
||||||
|
);
|
||||||
|
|
||||||
|
public record UpdateUnitInput(
|
||||||
|
string Name,
|
||||||
|
int Hexite,
|
||||||
|
int Flux,
|
||||||
|
int Supply,
|
||||||
|
int ProductionTime,
|
||||||
|
int Health,
|
||||||
|
int Energy,
|
||||||
|
int Shields,
|
||||||
|
int ArmorRating,
|
||||||
|
int MovementSpeed,
|
||||||
|
int DamagePerSecond,
|
||||||
|
int AttackRange,
|
||||||
|
IReadOnlyList<string>? Attributes,
|
||||||
|
int Tier,
|
||||||
|
string Faction,
|
||||||
|
string? Hotkey,
|
||||||
|
int BuildAtSameTime,
|
||||||
|
int? Limit
|
||||||
|
);
|
||||||
|
|
||||||
|
public class Mutation
|
||||||
|
{
|
||||||
|
public async Task<UnitData> CreateUnitAsync(CreateUnitInput input, ZeroSpaceDbContext db, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var exists = await db.Units.AnyAsync(u => u.Id == input.Id, cancellationToken);
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
throw new GraphQLException(ErrorBuilder.New().SetMessage($"Unit with id '{input.Id}' already exists.").SetCode("UNIT_ALREADY_EXISTS").Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
var unit = new UnitData(
|
||||||
|
input.Id,
|
||||||
|
input.Name,
|
||||||
|
input.Hexite,
|
||||||
|
input.Flux,
|
||||||
|
input.Supply,
|
||||||
|
input.ProductionTime,
|
||||||
|
input.Health,
|
||||||
|
input.Energy,
|
||||||
|
input.Shields,
|
||||||
|
input.ArmorRating,
|
||||||
|
input.MovementSpeed,
|
||||||
|
input.DamagePerSecond,
|
||||||
|
input.AttackRange,
|
||||||
|
input.Attributes ?? new List<string>(),
|
||||||
|
input.Tier,
|
||||||
|
input.Faction,
|
||||||
|
input.Hotkey,
|
||||||
|
input.BuildAtSameTime,
|
||||||
|
input.Limit
|
||||||
|
);
|
||||||
|
|
||||||
|
db.Units.Add(unit);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnitData> UpdateUnitAsync(string id, UpdateUnitInput input, ZeroSpaceDbContext db, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existing = await db.Units.FindAsync(new object[] { id }, cancellationToken);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
throw new GraphQLException(ErrorBuilder.New().SetMessage($"Unit with id '{id}' not found.").SetCode("UNIT_NOT_FOUND").Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated = new UnitData(
|
||||||
|
id,
|
||||||
|
input.Name,
|
||||||
|
input.Hexite,
|
||||||
|
input.Flux,
|
||||||
|
input.Supply,
|
||||||
|
input.ProductionTime,
|
||||||
|
input.Health,
|
||||||
|
input.Energy,
|
||||||
|
input.Shields,
|
||||||
|
input.ArmorRating,
|
||||||
|
input.MovementSpeed,
|
||||||
|
input.DamagePerSecond,
|
||||||
|
input.AttackRange,
|
||||||
|
input.Attributes ?? new List<string>(),
|
||||||
|
input.Tier,
|
||||||
|
input.Faction,
|
||||||
|
input.Hotkey,
|
||||||
|
input.BuildAtSameTime,
|
||||||
|
input.Limit
|
||||||
|
);
|
||||||
|
|
||||||
|
db.Units.Remove(existing);
|
||||||
|
db.Units.Add(updated);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUnitAsync(string id, ZeroSpaceDbContext db, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var unit = await db.Units.FindAsync(new object[] { id }, cancellationToken);
|
||||||
|
if (unit is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Units.Remove(unit);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using HotChocolate;
|
||||||
|
using HotChocolate.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ZeroSpace.ApiService.Data;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.ApiService.GraphQL;
|
||||||
|
|
||||||
|
public class Query
|
||||||
|
{
|
||||||
|
[UseProjection]
|
||||||
|
[UseFiltering]
|
||||||
|
[UseSorting]
|
||||||
|
public IQueryable<UnitData> GetUnits(ZeroSpaceDbContext db) =>
|
||||||
|
db.Units.AsNoTracking();
|
||||||
|
|
||||||
|
public async Task<UnitData?> GetUnitByIdAsync(string id, ZeroSpaceDbContext db, CancellationToken cancellationToken) =>
|
||||||
|
await db.Units.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using ZeroSpace.ApiService.Data;
|
||||||
|
using ZeroSpace.ApiService.Endpoints;
|
||||||
|
using ZeroSpace.ApiService.GraphQL;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add service defaults & Aspire components
|
||||||
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
|
// Add EF Core with PostgreSQL connection from Aspire ("zerospacedb")
|
||||||
|
builder.AddNpgsqlDbContext<ZeroSpaceDbContext>("zerospacedb");
|
||||||
|
|
||||||
|
// Add CORS for Web client
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddDefaultPolicy(policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Hot Chocolate GraphQL Server with Nitro UI (Banana Cake Pop)
|
||||||
|
builder.Services
|
||||||
|
.AddGraphQLServer()
|
||||||
|
.AddQueryType<Query>()
|
||||||
|
.AddMutationType<Mutation>()
|
||||||
|
.AddProjections()
|
||||||
|
.AddFiltering()
|
||||||
|
.AddSorting();
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.MapOpenApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map GraphQL endpoint with Nitro UI (Banana Cake Pop) at /graphql
|
||||||
|
app.MapGraphQL();
|
||||||
|
|
||||||
|
// Optional REST endpoints for auxiliary compatibility
|
||||||
|
app.MapUnitsEndpoints();
|
||||||
|
|
||||||
|
// Initialize database schema and initial data
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||||
|
await DbInitializer.InitializeDatabaseAsync(app.Services, app.Logger, cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
app.Logger.LogError(ex, "An error occurred while initializing the database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "https://localhost:7142;http://localhost:5142",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5142",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.5.4" />
|
||||||
|
<PackageReference Include="HotChocolate.AspNetCore" Version="16.6.7" />
|
||||||
|
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="16.6.7" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
|
<ProjectReference Include="..\ZeroSpace.ServiceDefaults\ZeroSpace.ServiceDefaults.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
var builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var postgres = builder.AddPostgres("postgres")
|
||||||
|
.WithDataVolume()
|
||||||
|
.AddDatabase("zerospacedb");
|
||||||
|
|
||||||
|
var apiService = builder.AddProject<Projects.ZeroSpace_ApiService>("apiservice")
|
||||||
|
.WithReference(postgres)
|
||||||
|
.WaitFor(postgres);
|
||||||
|
|
||||||
|
var web = builder.AddProject<Projects.ZeroSpace_Web>("web")
|
||||||
|
.WithReference(apiService)
|
||||||
|
.WaitFor(apiService);
|
||||||
|
|
||||||
|
builder.Build().Run();
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:17228;http://localhost:15228",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21145",
|
||||||
|
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:15228",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19145",
|
||||||
|
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Aspire.AppHost.Sdk/13.5.4">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsAspireHost>true</IsAspireHost>
|
||||||
|
<UserSecretsId>zerospace-apphost-secrets</UserSecretsId>
|
||||||
|
<NoWarn>$(NoWarn);ASPIRE010</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.5.4" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.5.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZeroSpace.ApiService\ZeroSpace.ApiService.csproj" />
|
||||||
|
<ProjectReference Include="..\ZeroSpace.Web\ZeroSpace.Web.csproj" />
|
||||||
|
<ProjectReference Include="..\ZeroSpace.ServiceDefaults\ZeroSpace.ServiceDefaults.csproj" IsAspireProjectResource="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"DetailedErrors": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"ResourceLifecycle": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"ResourceLifecycle": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ var solutionRoot = FindSolutionRoot();
|
|||||||
var docsRoot = args.Length > 0
|
var docsRoot = args.Length > 0
|
||||||
? Path.GetFullPath(args[0])
|
? Path.GetFullPath(args[0])
|
||||||
: Path.GetFullPath(Path.Combine(solutionRoot, "..", "zs.docs"));
|
: Path.GetFullPath(Path.Combine(solutionRoot, "..", "zs.docs"));
|
||||||
var modelRoot = Path.Combine(solutionRoot, "Model");
|
var modelRoot = Path.Combine(solutionRoot, "ZeroSpace.Model");
|
||||||
var outputPath = Path.Combine(modelRoot, "Units.g.cs");
|
var outputPath = Path.Combine(modelRoot, "Units.g.cs");
|
||||||
|
|
||||||
if (!Directory.Exists(docsRoot))
|
if (!Directory.Exists(docsRoot))
|
||||||
@@ -21,7 +21,7 @@ if (!Directory.Exists(modelRoot))
|
|||||||
var units = new List<UnitSource>();
|
var units = new List<UnitSource>();
|
||||||
var skipped = new List<string>();
|
var skipped = new List<string>();
|
||||||
|
|
||||||
foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md").Order(StringComparer.OrdinalIgnoreCase))
|
foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories).Order(StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var frontMatter = ReadFrontMatter(path);
|
var frontMatter = ReadFrontMatter(path);
|
||||||
if (frontMatter is null)
|
if (frontMatter is null)
|
||||||
@@ -71,19 +71,29 @@ if (skipped.Count > 0)
|
|||||||
|
|
||||||
static string FindSolutionRoot()
|
static string FindSolutionRoot()
|
||||||
{
|
{
|
||||||
var directory = new DirectoryInfo(Environment.CurrentDirectory);
|
var candidates = new[] { Environment.CurrentDirectory, AppContext.BaseDirectory };
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(candidate);
|
||||||
|
|
||||||
while (directory is not null)
|
while (directory is not null)
|
||||||
{
|
{
|
||||||
if (directory.EnumerateFiles("ZS.sln").Any())
|
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace.sln")))
|
||||||
{
|
{
|
||||||
return directory.FullName;
|
return directory.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
directory = directory.Parent;
|
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace", "ZeroSpace.sln")))
|
||||||
|
{
|
||||||
|
return Path.Combine(directory.FullName, "ZeroSpace");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find ZS.sln in the current directory or its parents.");
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not find ZeroSpace.sln in the current directory or its parents.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static Dictionary<string, FrontMatterValue>? ReadFrontMatter(string path)
|
static Dictionary<string, FrontMatterValue>? ReadFrontMatter(string path)
|
||||||
@@ -312,7 +322,7 @@ static string GenerateUnits(IReadOnlyList<UnitSource> units)
|
|||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.AppendLine("// <auto-generated />");
|
builder.AppendLine("// <auto-generated />");
|
||||||
builder.AppendLine("namespace Model;");
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
builder.AppendLine("public static class Units");
|
builder.AppendLine("public static class Units");
|
||||||
builder.AppendLine("{");
|
builder.AppendLine("{");
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
var deployDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
var deployDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||||
var webDir = Path.GetFullPath(Path.Combine(deployDir, "..", "Web"));
|
var webDir = Path.GetFullPath(Path.Combine(deployDir, "..", "ZeroSpace.Web"));
|
||||||
var webProject = Path.Combine(webDir, "Web.csproj");
|
var webProject = Path.Combine(webDir, "ZeroSpace.Web.csproj");
|
||||||
var publishDir = Path.Combine(Path.GetTempPath(), "zs-deploy", Guid.NewGuid().ToString());
|
var publishDir = Path.Combine(Path.GetTempPath(), "zs-deploy", Guid.NewGuid().ToString());
|
||||||
var deploymentToken = Environment.GetEnvironmentVariable("ZS_DeployToken") ?? throw new InvalidOperationException("ZS_DeployToken environment variable not set.");
|
var deploymentToken = Environment.GetEnvironmentVariable("ZS_DeployToken") ?? throw new InvalidOperationException("ZS_DeployToken environment variable not set.");
|
||||||
var deployEnv = Environment.GetEnvironmentVariable("ZS_DeployEnv") ?? "preview";
|
var deployEnv = Environment.GetEnvironmentVariable("ZS_DeployEnv") ?? "preview";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Model;
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
public sealed record UnitData(
|
public sealed record UnitData(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Model;
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
public static class UnitEfficiency
|
public static class UnitEfficiency
|
||||||
{
|
{
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
namespace Model;
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
public static class Units
|
public static class Units
|
||||||
{
|
{
|
||||||
@@ -150,48 +150,6 @@ public static class Units
|
|||||||
BuildAtSameTime: 1,
|
BuildAtSameTime: 1,
|
||||||
Limit: null);
|
Limit: null);
|
||||||
|
|
||||||
public static readonly UnitData Lasher = new(
|
|
||||||
Id: "Lasher",
|
|
||||||
Name: "Lasher",
|
|
||||||
Hexite: 50,
|
|
||||||
Flux: 25,
|
|
||||||
Supply: 2,
|
|
||||||
ProductionTime: 26,
|
|
||||||
Health: 150,
|
|
||||||
Energy: 0,
|
|
||||||
Shields: 0,
|
|
||||||
ArmorRating: 0,
|
|
||||||
MovementSpeed: 550,
|
|
||||||
DamagePerSecond: 14,
|
|
||||||
AttackRange: 8,
|
|
||||||
Attributes: ["Light Armor", "Biological", "Bonus Damage vs Medium Armor"],
|
|
||||||
Tier: 1,
|
|
||||||
Faction: "Grell",
|
|
||||||
Hotkey: "R",
|
|
||||||
BuildAtSameTime: 1,
|
|
||||||
Limit: null);
|
|
||||||
|
|
||||||
public static readonly UnitData Skrell = new(
|
|
||||||
Id: "Skrell",
|
|
||||||
Name: "Skrell",
|
|
||||||
Hexite: 12,
|
|
||||||
Flux: 19,
|
|
||||||
Supply: 1,
|
|
||||||
ProductionTime: 35,
|
|
||||||
Health: 50,
|
|
||||||
Energy: 0,
|
|
||||||
Shields: 0,
|
|
||||||
ArmorRating: 0,
|
|
||||||
MovementSpeed: 900,
|
|
||||||
DamagePerSecond: 5,
|
|
||||||
AttackRange: 2,
|
|
||||||
Attributes: ["Air unit", "Light Armor", "Biological", "Bonus Damage vs Air"],
|
|
||||||
Tier: 1,
|
|
||||||
Faction: "Grell",
|
|
||||||
Hotkey: "T",
|
|
||||||
BuildAtSameTime: 4,
|
|
||||||
Limit: null);
|
|
||||||
|
|
||||||
public static readonly UnitData Stinger = new(
|
public static readonly UnitData Stinger = new(
|
||||||
Id: "Stinger",
|
Id: "Stinger",
|
||||||
Name: "Stinger",
|
Name: "Stinger",
|
||||||
@@ -255,27 +213,6 @@ public static class Units
|
|||||||
BuildAtSameTime: 1,
|
BuildAtSameTime: 1,
|
||||||
Limit: null);
|
Limit: null);
|
||||||
|
|
||||||
public static readonly UnitData Thresher = new(
|
|
||||||
Id: "Thresher",
|
|
||||||
Name: "Thresher",
|
|
||||||
Hexite: 100,
|
|
||||||
Flux: 175,
|
|
||||||
Supply: 8,
|
|
||||||
ProductionTime: 65,
|
|
||||||
Health: 300,
|
|
||||||
Energy: 0,
|
|
||||||
Shields: 0,
|
|
||||||
ArmorRating: 0,
|
|
||||||
MovementSpeed: 450,
|
|
||||||
DamagePerSecond: 36,
|
|
||||||
AttackRange: 27,
|
|
||||||
Attributes: ["Biological", "Heavy Armor", "Bonus Damage vs Buildings"],
|
|
||||||
Tier: 2,
|
|
||||||
Faction: "Grell",
|
|
||||||
Hotkey: "S",
|
|
||||||
BuildAtSameTime: 1,
|
|
||||||
Limit: null);
|
|
||||||
|
|
||||||
public static readonly UnitData Behemoth = new(
|
public static readonly UnitData Behemoth = new(
|
||||||
Id: "Behemoth",
|
Id: "Behemoth",
|
||||||
Name: "Behemoth",
|
Name: "Behemoth",
|
||||||
@@ -348,12 +285,9 @@ public static class Units
|
|||||||
CorsairFlamer,
|
CorsairFlamer,
|
||||||
Gatherer,
|
Gatherer,
|
||||||
BroodGuard,
|
BroodGuard,
|
||||||
Lasher,
|
|
||||||
Skrell,
|
|
||||||
Stinger,
|
Stinger,
|
||||||
Harbinger,
|
Harbinger,
|
||||||
Mandragora,
|
Mandragora,
|
||||||
Thresher,
|
|
||||||
Behemoth,
|
Behemoth,
|
||||||
Reaver,
|
Reaver,
|
||||||
Weaver,
|
Weaver,
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OpenTelemetry;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
// Adds common .NET Aspire service defaults: OpenTelemetry, health checks, resilience, and service discovery.
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
builder.ConfigureOpenTelemetry();
|
||||||
|
|
||||||
|
builder.AddDefaultHealthChecks();
|
||||||
|
|
||||||
|
builder.Services.AddServiceDiscovery();
|
||||||
|
|
||||||
|
builder.Services.ConfigureHttpClientDefaults(http =>
|
||||||
|
{
|
||||||
|
// Turn on resilience by default
|
||||||
|
http.AddStandardResilienceHandler();
|
||||||
|
|
||||||
|
// Turn on service discovery by default
|
||||||
|
http.AddServiceDiscovery();
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
builder.Logging.AddOpenTelemetry(logging =>
|
||||||
|
{
|
||||||
|
logging.IncludeFormattedMessage = true;
|
||||||
|
logging.IncludeScopes = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddOpenTelemetry()
|
||||||
|
.WithMetrics(metrics =>
|
||||||
|
{
|
||||||
|
metrics.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddRuntimeInstrumentation();
|
||||||
|
})
|
||||||
|
.WithTracing(tracing =>
|
||||||
|
{
|
||||||
|
tracing.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.AddOpenTelemetryExporters();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
|
||||||
|
|
||||||
|
if (useOtlpExporter)
|
||||||
|
{
|
||||||
|
builder.Services.AddOpenTelemetry().UseOtlpExporter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||||
|
{
|
||||||
|
builder.Services.AddHealthChecks()
|
||||||
|
// Add a default liveness check to ensure app is responsive
|
||||||
|
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
// Adding health checks endpoints to applications in non-development environments has security implications.
|
||||||
|
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in other environments.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
// All health checks must pass for app to be considered ready to accept traffic after starting
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
|
// Only health checks tagged with the "live" tag must pass for app to be considered alive
|
||||||
|
app.MapHealthChecks("/alive", new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = r => r.Tags.Contains("live")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
app.MapHealthChecks("/alive", new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = r => r.Tags.Contains("live")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsAspireSharedProject>true</IsAspireSharedProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.10.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.10.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.19.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.19.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy project files for caching restore layer
|
||||||
|
COPY ["ZeroSpace.Web/ZeroSpace.Web.csproj", "ZeroSpace.Web/"]
|
||||||
|
COPY ["ZeroSpace.Model/ZeroSpace.Model.csproj", "ZeroSpace.Model/"]
|
||||||
|
|
||||||
|
RUN dotnet restore "ZeroSpace.Web/ZeroSpace.Web.csproj"
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/ZeroSpace.Web"
|
||||||
|
RUN dotnet publish "ZeroSpace.Web.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/publish/wwwroot .
|
||||||
|
COPY ZeroSpace.Web/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
@page "/units"
|
@page "/units"
|
||||||
@using Model
|
@using ZeroSpace.Model
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
@using Telerik.Blazor
|
@using Telerik.Blazor
|
||||||
@using Telerik.Blazor.Components
|
@using Telerik.Blazor.Components
|
||||||
|
@inject IUnitsApiClient UnitsApiClient
|
||||||
|
|
||||||
<PageTitle>Units</PageTitle>
|
<PageTitle>Units</PageTitle>
|
||||||
|
|
||||||
@@ -11,14 +12,29 @@
|
|||||||
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (gearNotes == null)
|
@if (isLoading)
|
||||||
{
|
{
|
||||||
<div class="d-flex justify-content-center py-5">
|
<div class="d-flex justify-content-center py-5">
|
||||||
<div class="spinner-border text-success" role="status">
|
<div class="spinner-border text-success" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading units...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
else if (errorMessage != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger d-flex align-items-center justify-content-between" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Error:</strong> @errorMessage
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-danger btn-sm" @onclick="LoadUnitsAsync">Retry</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (gearNotes == null || gearNotes.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
No units found.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
@@ -54,12 +70,31 @@ else
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<UnitData>? gearNotes;
|
private List<UnitData>? gearNotes;
|
||||||
|
private bool isLoading = true;
|
||||||
|
private string? errorMessage;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var index = Model.Units.All;
|
await LoadUnitsAsync();
|
||||||
gearNotes = index
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadUnitsAsync()
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var units = await UnitsApiClient.GetUnitsAsync();
|
||||||
|
gearNotes = units.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Failed to load units: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using ZeroSpace.Web;
|
||||||
|
using ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
||||||
|
{
|
||||||
|
var apiBaseUrl = builder.Configuration["services:apiservice:https:0"]
|
||||||
|
?? builder.Configuration["services:apiservice:http:0"]
|
||||||
|
?? builder.Configuration["ApiServiceUrl"]
|
||||||
|
?? builder.HostEnvironment.BaseAddress;
|
||||||
|
|
||||||
|
if (!apiBaseUrl.EndsWith('/'))
|
||||||
|
{
|
||||||
|
apiBaseUrl += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddTelerikBlazor();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public interface IUnitsApiClient
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<UnitData>> GetUnitsAsync(string? faction = null, int? tier = null, CancellationToken cancellationToken = default);
|
||||||
|
Task<UnitData?> GetUnitByIdAsync(string id, CancellationToken cancellationToken = default);
|
||||||
|
Task<UnitData?> CreateUnitAsync(UnitData unit, CancellationToken cancellationToken = default);
|
||||||
|
Task<bool> DeleteUnitAsync(string id, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public class UnitsApiClient : IUnitsApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<UnitsApiClient> _logger;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string AllUnitFields = @"
|
||||||
|
id
|
||||||
|
name
|
||||||
|
hexite
|
||||||
|
flux
|
||||||
|
supply
|
||||||
|
productionTime
|
||||||
|
health
|
||||||
|
energy
|
||||||
|
shields
|
||||||
|
armorRating
|
||||||
|
movementSpeed
|
||||||
|
damagePerSecond
|
||||||
|
attackRange
|
||||||
|
attributes
|
||||||
|
tier
|
||||||
|
faction
|
||||||
|
hotkey
|
||||||
|
buildAtSameTime
|
||||||
|
limit";
|
||||||
|
|
||||||
|
public UnitsApiClient(HttpClient httpClient, ILogger<UnitsApiClient> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UnitData>> GetUnitsAsync(string? faction = null, int? tier = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, object?>? whereFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction) || tier.HasValue)
|
||||||
|
{
|
||||||
|
whereFilter = new Dictionary<string, object?>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction))
|
||||||
|
{
|
||||||
|
whereFilter["faction"] = new Dictionary<string, object?> { ["eq"] = faction };
|
||||||
|
}
|
||||||
|
if (tier.HasValue)
|
||||||
|
{
|
||||||
|
whereFilter["tier"] = new Dictionary<string, object?> { ["eq"] = tier.Value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = $@"
|
||||||
|
query GetUnits($where: UnitDataFilterInput) {{
|
||||||
|
units(where: $where) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
where = whereFilter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<UnitsQueryResult>>(JsonOptions, cancellationToken);
|
||||||
|
if (result?.Data?.Units is not null && result.Data.Units.Count > 0)
|
||||||
|
{
|
||||||
|
return result.Data.Units;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load units via GraphQL API service. Falling back to static units list.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to static model data if GraphQL call fails
|
||||||
|
var fallback = Units.All.AsEnumerable();
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction))
|
||||||
|
{
|
||||||
|
fallback = fallback.Where(u => u.Faction.Equals(faction, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier.HasValue)
|
||||||
|
{
|
||||||
|
fallback = fallback.Where(u => u.Tier == tier.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnitData?> GetUnitByIdAsync(string id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = $@"
|
||||||
|
query GetUnitById($id: String!) {{
|
||||||
|
unitById(id: $id) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<UnitByIdQueryResult>>(JsonOptions, cancellationToken);
|
||||||
|
if (result?.Data?.UnitById is not null)
|
||||||
|
{
|
||||||
|
return result.Data.UnitById;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to get unit '{Id}' via GraphQL API.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Units.All.FirstOrDefault(u => u.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnitData?> CreateUnitAsync(UnitData unit, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mutation = $@"
|
||||||
|
mutation CreateUnit($input: CreateUnitInput!) {{
|
||||||
|
createUnit(input: $input) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var input = new
|
||||||
|
{
|
||||||
|
id = unit.Id,
|
||||||
|
name = unit.Name,
|
||||||
|
hexite = unit.Hexite,
|
||||||
|
flux = unit.Flux,
|
||||||
|
supply = unit.Supply,
|
||||||
|
productionTime = unit.ProductionTime,
|
||||||
|
health = unit.Health,
|
||||||
|
energy = unit.Energy,
|
||||||
|
shields = unit.Shields,
|
||||||
|
armorRating = unit.ArmorRating,
|
||||||
|
movementSpeed = unit.MovementSpeed,
|
||||||
|
damagePerSecond = unit.DamagePerSecond,
|
||||||
|
attackRange = unit.AttackRange,
|
||||||
|
attributes = unit.Attributes,
|
||||||
|
tier = unit.Tier,
|
||||||
|
faction = unit.Faction,
|
||||||
|
hotkey = unit.Hotkey,
|
||||||
|
buildAtSameTime = unit.BuildAtSameTime,
|
||||||
|
limit = unit.Limit
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query = mutation,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
input
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<CreateUnitMutationResult>>(JsonOptions, cancellationToken);
|
||||||
|
return result?.Data?.CreateUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogError("Failed to create unit via GraphQL. Status: {StatusCode}", response.StatusCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating unit via GraphQL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUnitAsync(string id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mutation = @"
|
||||||
|
mutation DeleteUnit($id: String!) {
|
||||||
|
deleteUnit(id: $id)
|
||||||
|
}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query = mutation,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<DeleteUnitMutationResult>>(JsonOptions, cancellationToken);
|
||||||
|
return result?.Data?.DeleteUnit ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting unit '{Id}' via GraphQL.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GraphQLResponse<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
public List<GraphQLErrorItem>? Errors { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GraphQLErrorItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnitsQueryResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("units")]
|
||||||
|
public List<UnitData>? Units { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnitByIdQueryResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("unitById")]
|
||||||
|
public UnitData? UnitById { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CreateUnitMutationResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("createUnit")]
|
||||||
|
public UnitData? CreateUnit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DeleteUnitMutationResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("deleteUnit")]
|
||||||
|
public bool DeleteUnit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
||||||
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0" />
|
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Web
|
@using ZeroSpace.Web
|
||||||
@using Web.Layout
|
@using ZeroSpace.Web.Layout
|
||||||
|
@using ZeroSpace.Model
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
events { }
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
types {
|
||||||
|
application/wasm wasm;
|
||||||
|
}
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|wasm|woff2?|eot|ttf|svg)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ApiServiceUrl": "https://localhost:7142"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ApiServiceUrl": "https://localhost:7142"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -11,7 +11,7 @@
|
|||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
|
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
|
||||||
<link href="Web.styles.css" rel="stylesheet" />
|
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user