Fix category shadow foreign keys and .NET 10 Docker build context
Build MoneyMap image / build-and-push (push) Failing after 7s
Build MoneyMap image / build-and-push (push) Failing after 7s
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Build with the repository root as context.
|
||||
.git
|
||||
**/bin
|
||||
**/obj
|
||||
**/.vs
|
||||
**/.idea
|
||||
**/TestResults
|
||||
**/*.user
|
||||
**/*.suo
|
||||
**/*.log
|
||||
**/node_modules
|
||||
|
||||
# Runtime configuration and credentials must be supplied outside the image.
|
||||
**/appsettings*.json
|
||||
**/secrets.json
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/*.pfx
|
||||
**/*.pem
|
||||
**/*.key
|
||||
|
||||
# Local receipts, database files, and backups are not application assets.
|
||||
**/receipts
|
||||
**/*.db
|
||||
**/*.db-*
|
||||
**/*.mdf
|
||||
**/*.ldf
|
||||
**/*.bak
|
||||
@@ -7,7 +7,8 @@ on:
|
||||
- "MoneyMap/**"
|
||||
- "MoneyMap.Core/**"
|
||||
- "MoneyMap.Mcp/**"
|
||||
- "Dockerfile"
|
||||
- "MoneyMap.sln"
|
||||
- ".dockerignore"
|
||||
- ".gitea/workflows/build-moneymap.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thecozycat.net -u "${{ gitea.actor }}" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -f MoneyMap/Dockerfile -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:${{ gitea.sha }} MoneyMap/
|
||||
run: docker build -f MoneyMap/Dockerfile -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/moneymap:${{ gitea.sha }} .
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
|
||||
@@ -1801,6 +1801,34 @@ var importer = new TransactionImporter(mockDb.Object, mockCardResolver.Object);
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker image build (.NET 10)
|
||||
|
||||
Run from the repository root (not `MoneyMap/`):
|
||||
|
||||
```bash
|
||||
docker build -f MoneyMap/Dockerfile -t moneymap:local .
|
||||
```
|
||||
|
||||
The Dockerfile uses the .NET 10 SDK and ASP.NET Core 10 runtime. The root
|
||||
context is required to copy `MoneyMap.sln` and the shared `MoneyMap.Core`
|
||||
project. The root `.dockerignore` excludes local build output, receipts,
|
||||
database files, and all `appsettings*.json`/credential files. Supply runtime
|
||||
configuration separately via environment variables (for example
|
||||
`ConnectionStrings__MoneyMapDb`, `OPENAI_API_KEY`) or a mounted
|
||||
configuration file; credentials are not baked into the image. Building does
|
||||
not start the application or apply migrations.
|
||||
|
||||
### Category relationship mapping
|
||||
|
||||
`Transaction.Category` and `CategoryMapping.Category` remain string category
|
||||
names. The corresponding collections on `Category` are `[NotMapped]` so EF
|
||||
cannot infer nonexistent `CategoryId` shadow foreign keys. The real
|
||||
`RecurringPlace.CategoryId` and parent/child category relationships remain
|
||||
mapped. This is a model correction only, with no database schema change or
|
||||
migration required. SQL Server model/query regression tests compile both
|
||||
receipt duplicate-check queries without connecting to a database.
|
||||
|
||||
|
||||
### Database Migration
|
||||
```bash
|
||||
# Create migration
|
||||
|
||||
@@ -25,7 +25,11 @@ public class Category
|
||||
public ICollection<Category> Children { get; set; } = new List<Category>();
|
||||
|
||||
public ICollection<RecurringPlace> RecurringPlaces { get; set; } = new List<RecurringPlace>();
|
||||
// These entities store category names, not Category foreign keys. Mapping these
|
||||
// collections would make EF invent CategoryId columns absent from the schema.
|
||||
[NotMapped]
|
||||
public ICollection<CategoryMapping> CategoryMappings { get; set; } = new List<CategoryMapping>();
|
||||
[NotMapped]
|
||||
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
|
||||
|
||||
[NotMapped]
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoneyMap.Data;
|
||||
using MoneyMap.Models;
|
||||
|
||||
namespace MoneyMap.Tests.Data;
|
||||
|
||||
public class CategoryModelTests
|
||||
{
|
||||
// Model/query compilation only: never opens a connection or reads application configuration.
|
||||
private static MoneyMapContext CreateContext() => new(
|
||||
new DbContextOptionsBuilder<MoneyMapContext>()
|
||||
.UseSqlServer("Server=localhost;Database=ModelOnly;Integrated Security=true;TrustServerCertificate=true")
|
||||
.Options);
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(Transaction))]
|
||||
[InlineData(typeof(CategoryMapping))]
|
||||
public void StringCategoryEntities_DoNotHaveCategoryForeignKeys(Type entityType)
|
||||
{
|
||||
using var context = CreateContext();
|
||||
var entity = context.Model.FindEntityType(entityType)!;
|
||||
|
||||
Assert.Null(entity.FindProperty("CategoryId"));
|
||||
Assert.DoesNotContain(entity.GetProperties(), p => p.IsShadowProperty());
|
||||
Assert.Equal(typeof(string), entity.FindProperty("Category")!.ClrType);
|
||||
Assert.DoesNotContain(entity.GetForeignKeys(), fk => fk.PrincipalEntityType.ClrType == typeof(Category));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ReceiptDuplicateQueries_DoNotSelectCategoryId(bool matchHash)
|
||||
{
|
||||
using var context = CreateContext();
|
||||
var query = context.Receipts.Include(r => r.Transaction);
|
||||
var sql = (matchHash
|
||||
? query.Where(r => r.FileHashSha256 == "test-hash")
|
||||
: query.Where(r => r.FileName == "receipt.png" && r.FileSizeBytes == 8))
|
||||
.ToQueryString();
|
||||
|
||||
Assert.Contains("LEFT JOIN [Transactions]", sql);
|
||||
Assert.Contains("[Category]", sql);
|
||||
Assert.DoesNotContain("[CategoryId]", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CategoryMappingQuery_DoesNotSelectCategoryId()
|
||||
{
|
||||
using var context = CreateContext();
|
||||
var sql = context.CategoryMappings.ToQueryString();
|
||||
Assert.Contains("[Category]", sql);
|
||||
Assert.DoesNotContain("[CategoryId]", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntendedCategoryRelationships_ArePreserved()
|
||||
{
|
||||
using var context = CreateContext();
|
||||
var recurringPlace = context.Model.FindEntityType(typeof(RecurringPlace))!;
|
||||
Assert.Contains(recurringPlace.GetForeignKeys(), fk =>
|
||||
fk.PrincipalEntityType.ClrType == typeof(Category) &&
|
||||
fk.Properties.Single().Name == "CategoryId" && !fk.Properties.Single().IsShadowProperty());
|
||||
var category = context.Model.FindEntityType(typeof(Category))!;
|
||||
Assert.Contains(category.GetForeignKeys(), fk =>
|
||||
fk.PrincipalEntityType.ClrType == typeof(Category) &&
|
||||
fk.Properties.Single().Name == "ParentCategoryId");
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Build stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy solution and project files for restore
|
||||
@@ -19,7 +19,7 @@ RUN libman restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Install ImageMagick dependencies for Magick.NET
|
||||
|
||||
Reference in New Issue
Block a user