Rewrite with React after AI got stuck in some obscure state errors on SolidJS

This commit is contained in:
2026-03-02 22:04:58 +02:00
parent 81c4c70c51
commit 154b9b66ce
38 changed files with 4131 additions and 1878 deletions

View File

@@ -65,6 +65,64 @@ public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture<ApiTestFa
var openHours = await getResponse.Content.ReadFromJsonAsync<List<LokOpenHoursDto>>();
Assert.NotNull(openHours);
Assert.Contains(openHours, item => item.Id == created.Id);
Assert.True(openHours.Count(item => item.IsActive) <= 1);
var createSecondPayload = new
{
id = 0,
name = "test-version-2",
version = DateTime.UtcNow.ToString("O"),
paragraph1 = "p1b",
paragraph2 = "p2b",
paragraph3 = "p3b",
paragraph4 = "p4b",
kitchenNotice = "k1b"
};
var createSecondResponse = await _client.PostAsJsonAsync("/lok/open-hours", createSecondPayload);
Assert.Equal(HttpStatusCode.Created, createSecondResponse.StatusCode);
var createdSecond = await createSecondResponse.Content.ReadFromJsonAsync<LokOpenHoursDto>();
Assert.NotNull(createdSecond);
Assert.True(createdSecond.IsActive);
var setActiveResponse = await _client.PutAsync($"/lok/open-hours/{created.Id}/active", null);
Assert.Equal(HttpStatusCode.OK, setActiveResponse.StatusCode);
var updatePayload = new
{
id = created.Id,
name = "updated-version",
version = DateTime.UtcNow.ToString("O"),
paragraph1 = "updated-p1",
paragraph2 = "updated-p2",
paragraph3 = "updated-p3",
paragraph4 = "updated-p4",
kitchenNotice = "updated-k1"
};
var updateResponse = await _client.PutAsJsonAsync($"/lok/open-hours/{created.Id}", updatePayload);
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
var updated = await updateResponse.Content.ReadFromJsonAsync<LokOpenHoursDto>();
Assert.NotNull(updated);
Assert.Equal(created.Id, updated.Id);
Assert.Equal(updatePayload.name, updated.Name);
Assert.Equal(updatePayload.paragraph1, updated.Paragraph1);
var getAfterUpdateResponse = await _client.GetAsync("/lok/open-hours");
Assert.Equal(HttpStatusCode.OK, getAfterUpdateResponse.StatusCode);
var openHoursAfterUpdate = await getAfterUpdateResponse.Content.ReadFromJsonAsync<List<LokOpenHoursDto>>();
Assert.NotNull(openHoursAfterUpdate);
var updatedInList = openHoursAfterUpdate.Single(item => item.Id == created.Id);
Assert.Equal(updatePayload.name, updatedInList.Name);
Assert.Equal(updatePayload.paragraph4, updatedInList.Paragraph4);
var activeCount = openHoursAfterUpdate.Count(item => item.IsActive);
Assert.Equal(1, activeCount);
Assert.False(openHoursAfterUpdate.Single(item => item.Id == createdSecond.Id).IsActive);
Assert.True(openHoursAfterUpdate.Single(item => item.Id == created.Id).IsActive);
var deleteResponse = await _client.DeleteAsync($"/lok/open-hours/{created.Id}");
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
@@ -117,6 +175,8 @@ public class LokOpenHoursDto
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime Version { get; set; }
public string Paragraph1 { get; set; } = string.Empty;

View File

@@ -71,5 +71,69 @@ public static class LokEndpoints
httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
})
.WithName("DeleteLokOpenHours");
app.MapPut("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) =>
{
var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>();
if (openHours is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Request body is required."
});
return;
}
if (string.IsNullOrWhiteSpace(openHours.Name))
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Open hours version name is required."
});
return;
}
var updatedOpenHours = await lokService.UpdateOpenHours(id, openHours);
if (updatedOpenHours is null)
{
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Open hours version not found."
});
return;
}
await httpContext.Response.WriteAsJsonAsync(updatedOpenHours);
})
.WithName("UpdateLokOpenHours");
app.MapPut("/lok/open-hours/{id:long}/active", async (HttpContext httpContext, long id) =>
{
var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var activated = await lokService.SetActiveOpenHours(id);
if (!activated)
{
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
await httpContext.Response.WriteAsJsonAsync(new
{
Message = "Open hours version not found."
});
return;
}
await httpContext.Response.WriteAsJsonAsync(new
{
Id = id,
IsActive = true
});
})
.WithName("SetActiveLokOpenHours");
}
}

View File

@@ -4,6 +4,8 @@ public class LokOpenHours
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime Version { get; set; }
public string Paragraph1 { get; set; } = string.Empty;

View File

@@ -21,6 +21,20 @@ public class Program
builder.Services.AddScoped(_ => new SqliteConnection(resolvedConnectionString));
builder.Services.AddScoped<LokService>();
builder.Services.AddCors(options =>
{
options.AddPolicy("UiCors", policy =>
{
policy
.WithOrigins(
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:4173",
"http://127.0.0.1:4173")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddOpenApi();
@@ -64,6 +78,57 @@ public class Program
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN name TEXT NOT NULL DEFAULT '';";
command.ExecuteNonQuery();
}
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('LokOpenHours') WHERE name = 'isActive';";
var hasIsActiveColumn = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!hasIsActiveColumn)
{
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN isActive INTEGER NOT NULL DEFAULT 0;";
command.ExecuteNonQuery();
}
command.CommandText = "SELECT COUNT(*) FROM LokOpenHours WHERE isActive = 1;";
var activeCount = Convert.ToInt32(command.ExecuteScalar());
if (activeCount == 0)
{
command.CommandText = @"
UPDATE LokOpenHours
SET isActive = 1
WHERE id = (
SELECT id
FROM LokOpenHours
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
);";
command.ExecuteNonQuery();
}
else if (activeCount > 1)
{
command.CommandText = @"
WITH selected_active AS (
SELECT id
FROM LokOpenHours
WHERE isActive = 1
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
)
UPDATE LokOpenHours
SET isActive = CASE
WHEN id = (SELECT id FROM selected_active) THEN 1
ELSE 0
END
WHERE isActive = 1
OR id = (SELECT id FROM selected_active);";
command.ExecuteNonQuery();
}
command.CommandText = @"
CREATE UNIQUE INDEX IF NOT EXISTS IX_LokOpenHours_OneActive
ON LokOpenHours(isActive)
WHERE isActive = 1;";
command.ExecuteNonQuery();
}
}
@@ -72,7 +137,12 @@ public class Program
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseCors("UiCors");
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
SystemEndpoints.MapSystemEndpoints(app);
LokEndpoints.MapLokEndpoints(app);

View File

@@ -19,7 +19,7 @@ public class LokService
await using var command = _connection.CreateCommand();
command.CommandText = @"
SELECT id, name, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice
SELECT id, name, isActive, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice
FROM LokOpenHours
ORDER BY datetime(version) DESC, id DESC
LIMIT 5";
@@ -34,6 +34,7 @@ public class LokService
{
Id = reader["id"] is long id ? id : Convert.ToInt64(reader["id"]),
Name = reader["name"]?.ToString() ?? string.Empty,
IsActive = ParseBoolean(reader["isActive"]),
Version = ParseVersion(reader["version"]?.ToString()),
Paragraph1 = reader["paragraph1"]?.ToString() ?? string.Empty,
Paragraph2 = reader["paragraph2"]?.ToString() ?? string.Empty,
@@ -55,13 +56,22 @@ public class LokService
var version = DateTime.UtcNow;
using var transaction = _connection.BeginTransaction();
await using var resetCommand = _connection.CreateCommand();
resetCommand.Transaction = transaction;
resetCommand.CommandText = "UPDATE LokOpenHours SET isActive = 0 WHERE isActive = 1;";
await resetCommand.ExecuteNonQueryAsync();
await using var command = _connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = @"
INSERT INTO LokOpenHours (name, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice)
VALUES (@name, @version, @paragraph1, @paragraph2, @paragraph3, @paragraph4, @kitchenNotice);
INSERT INTO LokOpenHours (name, isActive, version, paragraph1, paragraph2, paragraph3, paragraph4, kitchenNotice)
VALUES (@name, @isActive, @version, @paragraph1, @paragraph2, @paragraph3, @paragraph4, @kitchenNotice);
SELECT last_insert_rowid();";
command.Parameters.AddWithValue("@name", openHours.Name ?? string.Empty);
command.Parameters.AddWithValue("@isActive", 1);
command.Parameters.AddWithValue("@version", version.ToString("O"));
command.Parameters.AddWithValue("@paragraph1", openHours.Paragraph1 ?? string.Empty);
command.Parameters.AddWithValue("@paragraph2", openHours.Paragraph2 ?? string.Empty);
@@ -70,11 +80,15 @@ public class LokService
command.Parameters.AddWithValue("@kitchenNotice", openHours.KitchenNotice ?? string.Empty);
var insertedId = await command.ExecuteScalarAsync();
var insertedIdValue = Convert.ToInt64(insertedId);
transaction.Commit();
return new LokOpenHours
{
Id = Convert.ToInt64(insertedId),
Id = insertedIdValue,
Name = openHours.Name ?? string.Empty,
IsActive = true,
Version = version,
Paragraph1 = openHours.Paragraph1 ?? string.Empty,
Paragraph2 = openHours.Paragraph2 ?? string.Empty,
@@ -91,6 +105,16 @@ public class LokService
await _connection.OpenAsync();
}
await using var activeCommand = _connection.CreateCommand();
activeCommand.CommandText = "SELECT isActive FROM LokOpenHours WHERE id = @id;";
activeCommand.Parameters.AddWithValue("@id", id);
var activeValue = await activeCommand.ExecuteScalarAsync();
if (activeValue is null)
{
return false;
}
await using var command = _connection.CreateCommand();
command.CommandText = @"
DELETE FROM LokOpenHours
@@ -99,9 +123,115 @@ public class LokService
command.Parameters.AddWithValue("@id", id);
var affectedRows = await command.ExecuteNonQueryAsync();
if (affectedRows > 0)
{
await EnsureSingleActiveInvariant();
}
return affectedRows > 0;
}
public async Task<LokOpenHours?> UpdateOpenHours(long id, LokOpenHours openHours)
{
if (_connection.State != ConnectionState.Open)
{
await _connection.OpenAsync();
}
var version = DateTime.UtcNow;
await using var activeCommand = _connection.CreateCommand();
activeCommand.CommandText = "SELECT isActive FROM LokOpenHours WHERE id = @id;";
activeCommand.Parameters.AddWithValue("@id", id);
var activeValue = await activeCommand.ExecuteScalarAsync();
if (activeValue is null)
{
return null;
}
var isActive = ParseBoolean(activeValue);
await using var command = _connection.CreateCommand();
command.CommandText = @"
UPDATE LokOpenHours
SET
name = @name,
version = @version,
paragraph1 = @paragraph1,
paragraph2 = @paragraph2,
paragraph3 = @paragraph3,
paragraph4 = @paragraph4,
kitchenNotice = @kitchenNotice
WHERE id = @id;";
command.Parameters.AddWithValue("@id", id);
command.Parameters.AddWithValue("@name", openHours.Name ?? string.Empty);
command.Parameters.AddWithValue("@version", version.ToString("O"));
command.Parameters.AddWithValue("@paragraph1", openHours.Paragraph1 ?? string.Empty);
command.Parameters.AddWithValue("@paragraph2", openHours.Paragraph2 ?? string.Empty);
command.Parameters.AddWithValue("@paragraph3", openHours.Paragraph3 ?? string.Empty);
command.Parameters.AddWithValue("@paragraph4", openHours.Paragraph4 ?? string.Empty);
command.Parameters.AddWithValue("@kitchenNotice", openHours.KitchenNotice ?? string.Empty);
var affectedRows = await command.ExecuteNonQueryAsync();
if (affectedRows == 0)
{
return null;
}
return new LokOpenHours
{
Id = id,
Name = openHours.Name ?? string.Empty,
IsActive = isActive,
Version = version,
Paragraph1 = openHours.Paragraph1 ?? string.Empty,
Paragraph2 = openHours.Paragraph2 ?? string.Empty,
Paragraph3 = openHours.Paragraph3 ?? string.Empty,
Paragraph4 = openHours.Paragraph4 ?? string.Empty,
KitchenNotice = openHours.KitchenNotice ?? string.Empty
};
}
public async Task<bool> SetActiveOpenHours(long id)
{
if (_connection.State != ConnectionState.Open)
{
await _connection.OpenAsync();
}
using var transaction = _connection.BeginTransaction();
await using var existsCommand = _connection.CreateCommand();
existsCommand.Transaction = transaction;
existsCommand.CommandText = "SELECT COUNT(*) FROM LokOpenHours WHERE id = @id;";
existsCommand.Parameters.AddWithValue("@id", id);
var exists = Convert.ToInt32(await existsCommand.ExecuteScalarAsync()) > 0;
if (!exists)
{
transaction.Rollback();
return false;
}
await using var resetCommand = _connection.CreateCommand();
resetCommand.Transaction = transaction;
resetCommand.CommandText = "UPDATE LokOpenHours SET isActive = 0 WHERE isActive = 1;";
await resetCommand.ExecuteNonQueryAsync();
await using var activateCommand = _connection.CreateCommand();
activateCommand.Transaction = transaction;
activateCommand.CommandText = "UPDATE LokOpenHours SET isActive = 1 WHERE id = @id;";
activateCommand.Parameters.AddWithValue("@id", id);
await activateCommand.ExecuteNonQueryAsync();
transaction.Commit();
return true;
}
private static DateTime ParseVersion(string? value)
{
if (!string.IsNullOrWhiteSpace(value) && DateTime.TryParse(value, out var parsed))
@@ -111,4 +241,61 @@ public class LokService
return DateTime.MinValue;
}
private static bool ParseBoolean(object? value)
{
return value switch
{
bool boolValue => boolValue,
long longValue => longValue == 1,
int intValue => intValue == 1,
string stringValue when int.TryParse(stringValue, out var parsedInt) => parsedInt == 1,
string stringValue when bool.TryParse(stringValue, out var parsedBool) => parsedBool,
_ => false
};
}
private async Task EnsureSingleActiveInvariant()
{
await using var countCommand = _connection.CreateCommand();
countCommand.CommandText = "SELECT COUNT(*) FROM LokOpenHours WHERE isActive = 1;";
var activeCount = Convert.ToInt32(await countCommand.ExecuteScalarAsync());
if (activeCount == 0)
{
await using var promoteCommand = _connection.CreateCommand();
promoteCommand.CommandText = @"
UPDATE LokOpenHours
SET isActive = 1
WHERE id = (
SELECT id
FROM LokOpenHours
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
);";
await promoteCommand.ExecuteNonQueryAsync();
return;
}
if (activeCount > 1)
{
await using var normalizeCommand = _connection.CreateCommand();
normalizeCommand.CommandText = @"
WITH selected_active AS (
SELECT id
FROM LokOpenHours
WHERE isActive = 1
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
)
UPDATE LokOpenHours
SET isActive = CASE
WHEN id = (SELECT id FROM selected_active) THEN 1
ELSE 0
END
WHERE isActive = 1
OR id = (SELECT id FROM selected_active);";
await normalizeCommand.ExecuteNonQueryAsync();
}
}
}

View File

@@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS LokOpenHours (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
isActive INTEGER NOT NULL DEFAULT 0,
version TEXT NOT NULL,
paragraph1 TEXT NOT NULL DEFAULT '',
paragraph2 TEXT NOT NULL DEFAULT '',

Binary file not shown.