All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 4m4s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m29s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
261 lines
9.6 KiB
C#
261 lines
9.6 KiB
C#
using Dapper;
|
|
using Mindforge.API.Models.Flashcards;
|
|
using Mindforge.API.Services.Interfaces;
|
|
using Npgsql;
|
|
|
|
namespace Mindforge.API.Repositories
|
|
{
|
|
public class FlashcardRepository : IFlashcardRepository
|
|
{
|
|
private const string DefaultConnectionString = "Host=iris.haven;Database=mindforge;Username=root;Password=root";
|
|
private readonly string _connectionString;
|
|
|
|
public FlashcardRepository(IConfiguration configuration)
|
|
{
|
|
_connectionString = configuration.GetConnectionString("MindforgeDb")
|
|
?? configuration["MINDFORGE_DB_CONNECTION"]
|
|
?? DefaultConnectionString;
|
|
}
|
|
|
|
public async Task EnsureSchemaAsync()
|
|
{
|
|
const string sql = """
|
|
CREATE TABLE IF NOT EXISTS flashcard_libraries (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
file_path TEXT NOT NULL UNIQUE,
|
|
file_name TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
difficulty TEXT NOT NULL,
|
|
card_count INTEGER NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS flashcards (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
library_id BIGINT NOT NULL REFERENCES flashcard_libraries(id) ON DELETE CASCADE,
|
|
front TEXT NOT NULL,
|
|
back TEXT NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_flashcard_libraries_subject ON flashcard_libraries(subject);
|
|
CREATE INDEX IF NOT EXISTS ix_flashcards_library_id_position ON flashcards(library_id, position);
|
|
""";
|
|
|
|
await using var connection = CreateConnection();
|
|
await connection.OpenAsync();
|
|
await connection.ExecuteAsync(sql);
|
|
}
|
|
|
|
public async Task<FlashcardLibraryDetails> UpsertLibraryAsync(
|
|
string filePath,
|
|
string fileName,
|
|
string subject,
|
|
string difficulty,
|
|
IReadOnlyList<FlashcardDraftCard> cards)
|
|
{
|
|
await using var connection = CreateConnection();
|
|
await connection.OpenAsync();
|
|
await using var transaction = await connection.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
var existingId = await connection.ExecuteScalarAsync<long?>(
|
|
"SELECT id FROM flashcard_libraries WHERE file_path = @FilePath FOR UPDATE;",
|
|
new { FilePath = filePath },
|
|
transaction);
|
|
|
|
long libraryId;
|
|
if (existingId.HasValue)
|
|
{
|
|
libraryId = existingId.Value;
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE flashcard_libraries
|
|
SET file_name = @FileName,
|
|
subject = @Subject,
|
|
difficulty = @Difficulty,
|
|
card_count = @CardCount,
|
|
updated_at = NOW()
|
|
WHERE id = @LibraryId;
|
|
""",
|
|
new
|
|
{
|
|
FileName = fileName,
|
|
Subject = subject,
|
|
Difficulty = difficulty,
|
|
CardCount = cards.Count,
|
|
LibraryId = libraryId
|
|
},
|
|
transaction);
|
|
|
|
await connection.ExecuteAsync(
|
|
"DELETE FROM flashcards WHERE library_id = @LibraryId;",
|
|
new { LibraryId = libraryId },
|
|
transaction);
|
|
}
|
|
else
|
|
{
|
|
libraryId = await connection.ExecuteScalarAsync<long>(
|
|
"""
|
|
INSERT INTO flashcard_libraries (file_path, file_name, subject, difficulty, card_count)
|
|
VALUES (@FilePath, @FileName, @Subject, @Difficulty, @CardCount)
|
|
RETURNING id;
|
|
""",
|
|
new
|
|
{
|
|
FilePath = filePath,
|
|
FileName = fileName,
|
|
Subject = subject,
|
|
Difficulty = difficulty,
|
|
CardCount = cards.Count
|
|
},
|
|
transaction);
|
|
}
|
|
|
|
if (cards.Count > 0)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO flashcards (library_id, front, back, position)
|
|
VALUES (@LibraryId, @Front, @Back, @Position);
|
|
""",
|
|
cards.Select(card => new
|
|
{
|
|
LibraryId = libraryId,
|
|
card.Front,
|
|
card.Back,
|
|
card.Position
|
|
}),
|
|
transaction);
|
|
}
|
|
|
|
await transaction.CommitAsync();
|
|
var details = await GetLibraryByIdAsync(libraryId);
|
|
if (details is null)
|
|
{
|
|
throw new InvalidOperationException("Biblioteca gerada nao encontrada apos persistencia.");
|
|
}
|
|
|
|
return details;
|
|
}
|
|
catch
|
|
{
|
|
await transaction.RollbackAsync();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyList<FlashcardLibrarySummary>> GetLibrariesAsync()
|
|
{
|
|
const string sql = """
|
|
SELECT
|
|
id AS Id,
|
|
file_path AS FilePath,
|
|
file_name AS FileName,
|
|
subject AS Subject,
|
|
difficulty AS Difficulty,
|
|
card_count AS CardCount,
|
|
created_at AS CreatedAt,
|
|
updated_at AS UpdatedAt
|
|
FROM flashcard_libraries
|
|
ORDER BY subject, file_name;
|
|
""";
|
|
|
|
await using var connection = CreateConnection();
|
|
await connection.OpenAsync();
|
|
var rows = await connection.QueryAsync<FlashcardLibrarySummary>(sql);
|
|
return rows.ToList();
|
|
}
|
|
|
|
public async Task<FlashcardLibraryDetails?> GetLibraryByIdAsync(long libraryId)
|
|
{
|
|
const string librarySql = """
|
|
SELECT
|
|
id AS Id,
|
|
file_path AS FilePath,
|
|
file_name AS FileName,
|
|
subject AS Subject,
|
|
difficulty AS Difficulty,
|
|
card_count AS CardCount,
|
|
created_at AS CreatedAt,
|
|
updated_at AS UpdatedAt
|
|
FROM flashcard_libraries
|
|
WHERE id = @LibraryId;
|
|
""";
|
|
|
|
const string cardsSql = """
|
|
SELECT
|
|
id AS Id,
|
|
library_id AS LibraryId,
|
|
front AS Front,
|
|
back AS Back,
|
|
position AS Position,
|
|
created_at AS CreatedAt
|
|
FROM flashcards
|
|
WHERE library_id = @LibraryId
|
|
ORDER BY position;
|
|
""";
|
|
|
|
await using var connection = CreateConnection();
|
|
await connection.OpenAsync();
|
|
|
|
var library = await connection.QuerySingleOrDefaultAsync<FlashcardLibrarySummary>(
|
|
librarySql,
|
|
new { LibraryId = libraryId });
|
|
|
|
if (library is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var cards = await connection.QueryAsync<FlashcardCard>(cardsSql, new { LibraryId = libraryId });
|
|
return new FlashcardLibraryDetails
|
|
{
|
|
Id = library.Id,
|
|
FilePath = library.FilePath,
|
|
FileName = library.FileName,
|
|
Subject = library.Subject,
|
|
Difficulty = library.Difficulty,
|
|
CardCount = library.CardCount,
|
|
CreatedAt = library.CreatedAt,
|
|
UpdatedAt = library.UpdatedAt,
|
|
Cards = cards.ToList()
|
|
};
|
|
}
|
|
|
|
public async Task<IReadOnlyList<FlashcardCard>> GetCardsForLibrariesAsync(IReadOnlyList<long> libraryIds)
|
|
{
|
|
if (libraryIds.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
const string sql = """
|
|
SELECT
|
|
id AS Id,
|
|
library_id AS LibraryId,
|
|
front AS Front,
|
|
back AS Back,
|
|
position AS Position,
|
|
created_at AS CreatedAt
|
|
FROM flashcards
|
|
WHERE library_id = ANY(@LibraryIds)
|
|
ORDER BY library_id, position;
|
|
""";
|
|
|
|
await using var connection = CreateConnection();
|
|
await connection.OpenAsync();
|
|
var cards = await connection.QueryAsync<FlashcardCard>(sql, new { LibraryIds = libraryIds.ToArray() });
|
|
return cards.ToList();
|
|
}
|
|
|
|
private NpgsqlConnection CreateConnection()
|
|
{
|
|
return new NpgsqlConnection(_connectionString);
|
|
}
|
|
}
|
|
}
|