Skip to content

Commit d37db26

Browse files
Merge pull request #299 from CodebreakerApp/298-postgres
298 PostgreSQL library with build on DevOps
2 parents 6fa1390 + ec6c267 commit d37db26

File tree

12 files changed

+390
-0
lines changed

12 files changed

+390
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: PostgreSQL data lib
2+
3+
on:
4+
5+
# Automatically trigger it when detected changes in repo
6+
push:
7+
branches:
8+
[ main ]
9+
paths:
10+
- 'src/services/common/Codebreaker.Data.PostgreSQL/**'
11+
12+
# Allow manually trigger
13+
workflow_dispatch:
14+
15+
jobs:
16+
build:
17+
uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/createnuget-withbuildnumber.yml@main
18+
with:
19+
version-suffix: preview.1.
20+
version-number: ${{ github.run_number }}
21+
version-offset: 5
22+
solutionfile-path: src/Codebreaker.Backend.PostgreSQL.sln
23+
projectfile-path: src/services/common/Codebreaker.Data.PostgreSQL/Codebreaker.Data.PostgreSQL.csproj
24+
dotnet-version: '9.0.x'
25+
artifact-name: codebreaker-postgres
26+
branch-name: main
27+
28+
publishdevops:
29+
uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/publishnuget-azuredevops.yml@main
30+
needs: build
31+
with:
32+
artifact-name: codebreaker-postgres
33+
secrets: inherit
34+
35+
# publishnuget:
36+
# uses: CodebreakerApp/Codebreaker.Backend/.github/workflows/publishnuget-nugetserver.yml@main
37+
# needs: publishdevops
38+
# with:
39+
# artifact-name: codebreaker-sqlserver
40+
# secrets: inherit
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebreaker.Data.PostgreSQL", "services\common\Codebreaker.Data.PostgreSQL\Codebreaker.Data.PostgreSQL.csproj", "{4557EA75-02C8-B4BF-67BA-F4CA4DF1C78C}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{4557EA75-02C8-B4BF-67BA-F4CA4DF1C78C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{4557EA75-02C8-B4BF-67BA-F4CA4DF1C78C}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{4557EA75-02C8-B4BF-67BA-F4CA4DF1C78C}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{4557EA75-02C8-B4BF-67BA-F4CA4DF1C78C}.Release|Any CPU.Build.0 = Release|Any CPU
20+
EndGlobalSection
21+
GlobalSection(SolutionProperties) = preSolution
22+
HideSolutionNode = FALSE
23+
EndGlobalSection
24+
GlobalSection(ExtensibilityGlobals) = postSolution
25+
SolutionGuid = {B92B3E5A-CD74-42B9-A371-11A3256C6CEF}
26+
EndGlobalSection
27+
EndGlobal
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"dotnet-ef": {
6+
"version": "8.0.0-preview.5.23280.1",
7+
"commands": [
8+
"dotnet-ef"
9+
]
10+
}
11+
}
12+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageId>CNinnovation.Codebreaker.PostgreSQL</PackageId>
5+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<LangVersion>latest</LangVersion>
8+
<Nullable>enable</Nullable>
9+
<PackageTags>
10+
Codebreaker;CNinnovation;PostgreSQL
11+
</PackageTags>
12+
<Description>
13+
This library contains PostgreSQL data access for the Codebreaker backend services.
14+
See https://github.com/codebreakerapp for more information on the complete solution.
15+
</Description>
16+
<PackageReadmeFile>readme.md</PackageReadmeFile>
17+
<PackageIcon>codebreaker.jpeg</PackageIcon>
18+
<Version>3.8.0</Version>
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="CNinnovation.Codebreaker.BackendModels" Version="3.8.0" />
23+
</ItemGroup>
24+
25+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
26+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
27+
</ItemGroup>
28+
29+
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
30+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<None Include="docs/readme.md" Pack="true" PackagePath="\" />
35+
<None Include="Images/codebreaker.jpeg" Pack="true" PackagePath="\" />
36+
</ItemGroup>
37+
38+
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.EntityFrameworkCore.ChangeTracking;
2+
3+
namespace Codebreaker.Data.Postgres;
4+
5+
internal class GameConfiguration : IEntityTypeConfiguration<Game>
6+
{
7+
public void Configure(EntityTypeBuilder<Game> builder)
8+
{
9+
builder.HasKey(g => g.Id);
10+
11+
builder.HasMany(g => g.Moves)
12+
.WithOne()
13+
.HasForeignKey("GameId");
14+
15+
builder.Property(g => g.GameType).HasMaxLength(20);
16+
builder.Property(g => g.PlayerName).HasMaxLength(60);
17+
18+
builder.Property(g => g.Codes).HasMaxLength(120);
19+
20+
builder.Property(g => g.StartTime)
21+
.HasConversion(
22+
v => v.ToUniversalTime(),
23+
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
24+
25+
builder.Property(g => g.EndTime)
26+
.HasConversion(
27+
v => v.HasValue ? v.Value.ToUniversalTime() : v,
28+
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);
29+
30+
31+
builder.Property(g => g.FieldValues)
32+
.HasColumnName("Fields")
33+
.HasConversion(convertToProviderExpression: fields => fields.ToFieldsString(),
34+
convertFromProviderExpression: fields => fields.FromFieldsString(),
35+
valueComparer: new ValueComparer<IDictionary<string, IEnumerable<string>>>(
36+
equalsExpression: (a, b) => a!.SequenceEqual(b!),
37+
hashCodeExpression: a => a.Aggregate(0, (result, next) => HashCode.Combine(result, next.GetHashCode())),
38+
snapshotExpression: a => a.ToDictionary(kv => kv.Key, kv => kv.Value)));
39+
}
40+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Codebreaker.Data.Postgres;
2+
3+
internal class MoveConfiguration : IEntityTypeConfiguration<Move>
4+
{
5+
public void Configure(EntityTypeBuilder<Move> builder)
6+
{
7+
// shadow property for the foreign key
8+
builder.Property<Guid>("GameId");
9+
10+
builder.Property(g => g.GuessPegs).HasMaxLength(120);
11+
builder.Property(g => g.KeyPegs).HasMaxLength(60);
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project>
2+
<PropertyGroup>
3+
<!-- Enable central package management, https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
4+
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
</ItemGroup>
8+
</Project>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using Codebreaker.GameAPIs.Data;
2+
3+
namespace Codebreaker.Data.Postgres;
4+
5+
public class GamesPostgresContext(DbContextOptions<GamesPostgresContext> options) : DbContext(options), IGamesRepository
6+
{
7+
protected override void OnModelCreating(ModelBuilder modelBuilder)
8+
{
9+
modelBuilder.HasDefaultSchema("codebreaker");
10+
modelBuilder.ApplyConfiguration(new GameConfiguration());
11+
modelBuilder.ApplyConfiguration(new MoveConfiguration());
12+
}
13+
14+
public DbSet<Game> Games => Set<Game>();
15+
public DbSet<Move> Moves => Set<Move>();
16+
17+
public async Task AddGameAsync(Game game, CancellationToken cancellationToken = default)
18+
{
19+
Games.Add(game);
20+
await SaveChangesAsync(cancellationToken);
21+
}
22+
23+
public async Task AddMoveAsync(Game game, Move move, CancellationToken cancellationToken = default)
24+
{
25+
Moves.Add(move);
26+
Games.Update(game);
27+
28+
await SaveChangesAsync(cancellationToken);
29+
}
30+
31+
public async Task<bool> DeleteGameAsync(Guid id, CancellationToken cancellationToken = default)
32+
{
33+
var affected = await Games
34+
.Where(g => g.Id == id)
35+
.ExecuteDeleteAsync(cancellationToken);
36+
return affected == 1;
37+
}
38+
39+
public async Task<Game?> GetGameAsync(Guid id, CancellationToken cancellationToken = default)
40+
{
41+
var game = await Games
42+
.Include("Moves")
43+
.TagWith(nameof(GetGameAsync))
44+
.SingleOrDefaultAsync(g => g.Id == id, cancellationToken);
45+
return game;
46+
}
47+
48+
public async Task<IEnumerable<Game>> GetGamesByDateAsync(string gameType, DateOnly date, CancellationToken cancellationToken = default)
49+
{
50+
var d = date.ToDateTime(TimeOnly.MinValue);
51+
var games = await Games
52+
.Where(g => g.GameType == gameType && g.StartTime.Date == d)
53+
.TagWith(nameof(GetGamesByDateAsync))
54+
.ToListAsync(cancellationToken);
55+
return games;
56+
}
57+
58+
public async Task<IEnumerable<Game>> GetGamesByPlayerAsync(string playerName, CancellationToken cancellationToken = default)
59+
{
60+
var games = await Games
61+
.Where(g => g.PlayerName == playerName)
62+
.TagWith(nameof(GetGamesByPlayerAsync))
63+
.ToListAsync(cancellationToken);
64+
return games;
65+
}
66+
67+
public async Task<IEnumerable<Game>> GetRunningGamesByPlayerAsync(string playerName, CancellationToken cancellationToken = default)
68+
{
69+
var games = await Games
70+
.Where(g => g.PlayerName == playerName && g.EndTime == null)
71+
.ToListAsync(cancellationToken);
72+
return games;
73+
}
74+
75+
private const int MaxGamesReturned = 500;
76+
77+
public async Task<IEnumerable<Game>> GetGamesAsync(GamesQuery gamesQuery, CancellationToken cancellationToken = default)
78+
{
79+
IQueryable<Game> query = Games
80+
.TagWith(nameof(GetGamesAsync))
81+
.Include(g => g.Moves);
82+
83+
// Apply Game filters if provided.
84+
if (gamesQuery.Date.HasValue)
85+
{
86+
DateTime begin = gamesQuery.Date.Value.ToDateTime(TimeOnly.MinValue);
87+
DateTime end = begin.AddDays(1);
88+
query = query.Where(g => g.StartTime < end && g.StartTime > begin);
89+
}
90+
if (gamesQuery.PlayerName != null)
91+
{
92+
query = query.Where(g => g.PlayerName == gamesQuery.PlayerName);
93+
}
94+
if (gamesQuery.GameType != null)
95+
{
96+
query = query.Where(g => g.GameType == gamesQuery.GameType);
97+
}
98+
if (gamesQuery.RunningOnly)
99+
{
100+
query = query.Where(g => g.EndTime == null);
101+
}
102+
if (gamesQuery.Ended)
103+
{
104+
query = query.Where(g => g.EndTime != null)
105+
.OrderBy(g => g.Duration);
106+
}
107+
else
108+
{
109+
query = query.OrderByDescending(g => g.StartTime);
110+
}
111+
112+
query = query.Take(MaxGamesReturned);
113+
114+
return await query.ToListAsync(cancellationToken);
115+
}
116+
117+
public async Task<Game> UpdateGameAsync(Game game, CancellationToken cancellationToken = default)
118+
{
119+
Games.Update(game);
120+
await SaveChangesAsync(cancellationToken);
121+
return game;
122+
}
123+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
global using Codebreaker.GameAPIs.Models;
2+
global using Microsoft.EntityFrameworkCore;
3+
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
11.4 KB
Loading

0 commit comments

Comments
 (0)