Updating structure
This commit is contained in:
@@ -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": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user