diff options
Diffstat (limited to 'services/app/src/Repositories')
| -rw-r--r-- | services/app/src/Repositories/AnswerRepository.php | 277 | ||||
| -rw-r--r-- | services/app/src/Repositories/QuizRepository.php | 234 | ||||
| -rw-r--r-- | services/app/src/Repositories/TestcaseExecutionRepository.php | 296 | ||||
| -rw-r--r-- | services/app/src/Repositories/TestcaseRepository.php | 143 | ||||
| -rw-r--r-- | services/app/src/Repositories/UserRepository.php | 127 |
5 files changed, 1077 insertions, 0 deletions
diff --git a/services/app/src/Repositories/AnswerRepository.php b/services/app/src/Repositories/AnswerRepository.php new file mode 100644 index 0000000..1798084 --- /dev/null +++ b/services/app/src/Repositories/AnswerRepository.php @@ -0,0 +1,277 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Repositories; + +use DateTimeImmutable; +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Models\AggregatedExecutionStatus; +use Nsfisis\Albatross\Models\Answer; +use Nsfisis\Albatross\Sql\DateTimeParser; +use PDOException; + +final class AnswerRepository +{ + private const ANSWER_FIELDS = [ + 'answer_id', + 'quiz_id', + 'answer_number', + 'submitted_at', + 'author_id', + 'code', + 'code_size', + 'execution_status', + ]; + + private const ANSWER_JOIN_USER_FIELDS = [ + 'users.username AS author_name', + 'users.is_admin AS author_is_admin', + ]; + + public function __construct( + private readonly Connection $conn, + ) { + } + + /** + * @return Answer[] + */ + public function listByQuizId(int $quiz_id): array + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('quiz_id = :quiz_id') + ->orderBy([['execution_status', 'DESC'], ['code_size', 'ASC'], ['submitted_at', 'ASC']]) + ->execute(['quiz_id' => $quiz_id]); + return array_map($this->mapRawRowToAnswer(...), $result); + } + + /** + * @return Answer[] + */ + public function listByQuizIdAndAuthorId(int $quiz_id, int $author_id): array + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('quiz_id = :quiz_id AND author_id = :author_id') + ->orderBy([['execution_status', 'DESC'], ['code_size', 'ASC'], ['submitted_at', 'ASC']]) + ->execute(['quiz_id' => $quiz_id, 'author_id' => $author_id]); + return array_map($this->mapRawRowToAnswer(...), $result); + } + + public function findByQuizIdAndAnswerNumber(int $quiz_id, int $answer_number): ?Answer + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('quiz_id = :quiz_id AND answer_number = :answer_number') + ->first() + ->execute(['quiz_id' => $quiz_id, 'answer_number' => $answer_number]); + return isset($result) ? $this->mapRawRowToAnswer($result) : null; + } + + public function findById(int $answer_id): ?Answer + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('answer_id = :answer_id') + ->first() + ->execute(['answer_id' => $answer_id]); + return isset($result) ? $this->mapRawRowToAnswer($result) : null; + } + + /** + * @param positive-int $upto + * @return Answer[] + */ + public function getRanking(int $quiz_id, int $upto): array + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('quiz_id = :quiz_id AND execution_status = :execution_status') + ->orderBy([['code_size', 'ASC'], ['submitted_at', 'ASC']]) + ->limit($upto) + ->execute(['quiz_id' => $quiz_id, 'execution_status' => AggregatedExecutionStatus::OK->toInt()]); + return array_map($this->mapRawRowToAnswer(...), $result); + } + + /** + * @return Answer[] + */ + public function listAllCorrectAnswers(int $quiz_id): array + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('quiz_id = :quiz_id AND execution_status = :execution_status') + ->orderBy([['submitted_at', 'ASC']]) + ->execute(['quiz_id' => $quiz_id, 'execution_status' => AggregatedExecutionStatus::OK->toInt()]); + return array_map($this->mapRawRowToAnswer(...), $result); + } + + public function create( + int $quiz_id, + int $author_id, + string $code, + ): int { + $answer = Answer::create( + quiz_id: $quiz_id, + author_id: $author_id, + code: $code, + ); + + $next_answer_number_query = $this->conn + ->query() + ->select('answers') + ->fields(['COALESCE(MAX(answer_number), 0) + 1']) + ->where('quiz_id = :quiz_id') + ->limit(1); + + try { + return $this->conn + ->query() + ->insert('answers') + ->values([ + 'quiz_id' => $answer->quiz_id, + 'answer_number' => $next_answer_number_query, + 'author_id' => $answer->author_id, + 'code' => $answer->code, + 'code_size' => $answer->code_size, + 'execution_status' => $answer->execution_status->toInt(), + ]) + ->execute(); + } catch (PDOException $e) { + throw new EntityValidationException( + message: '回答の投稿に失敗しました', + previous: $e, + ); + } + } + + public function markAllAsPending(int $quiz_id): void + { + $this->conn + ->query() + ->update('answers') + ->set(['execution_status' => AggregatedExecutionStatus::Pending->toInt()]) + ->where('quiz_id = :quiz_id') + ->execute(['quiz_id' => $quiz_id]); + } + + public function markAllAsUpdateNeeded(int $quiz_id): void + { + $this->conn + ->query() + ->update('answers') + ->set(['execution_status' => AggregatedExecutionStatus::UpdateNeeded->toInt()]) + ->where('quiz_id = :quiz_id') + ->execute(['quiz_id' => $quiz_id]); + } + + public function markAsPending(int $answer_id): void + { + $this->conn + ->query() + ->update('answers') + ->set(['execution_status' => AggregatedExecutionStatus::Pending->toInt()]) + ->where('answer_id = :answer_id') + ->execute(['answer_id' => $answer_id]); + } + + public function tryGetNextUpdateNeededAnswer(): ?Answer + { + $result = $this->conn + ->query() + ->select('answers') + ->leftJoin('users', 'answers.author_id = users.user_id') + ->fields([...self::ANSWER_FIELDS, ...self::ANSWER_JOIN_USER_FIELDS]) + ->where('execution_status = :execution_status') + ->orderBy([['submitted_at', 'ASC']]) + ->first() + ->execute(['execution_status' => AggregatedExecutionStatus::UpdateNeeded->toInt()]); + return isset($result) ? $this->mapRawRowToAnswer($result) : null; + } + + public function updateExecutionStatus( + int $answer_id, + AggregatedExecutionStatus $execution_status, + ): void { + $this->conn + ->query() + ->update('answers') + ->set(['execution_status' => $execution_status->toInt()]) + ->where('answer_id = :answer_id') + ->execute(['answer_id' => $answer_id]); + } + + public function deleteAllByQuizId(int $quiz_id): void + { + $this->conn + ->query() + ->delete('answers') + ->where('quiz_id = :quiz_id') + ->execute(['quiz_id' => $quiz_id]); + } + + public function deleteAllByUserId(int $user_id): void + { + $this->conn + ->query() + ->delete('answers') + ->where('author_id = :author_id') + ->execute(['author_id' => $user_id]); + } + + /** + * @param array<string, ?string> $row + */ + private function mapRawRowToAnswer(array $row): Answer + { + assert(isset($row['answer_id'])); + assert(isset($row['quiz_id'])); + assert(isset($row['answer_number'])); + assert(isset($row['submitted_at'])); + assert(isset($row['author_id'])); + assert(isset($row['code'])); + assert(isset($row['code_size'])); + assert(isset($row['execution_status'])); + + $answer_id = (int) $row['answer_id']; + $quiz_id = (int) $row['quiz_id']; + $answer_number = (int) $row['answer_number']; + $submitted_at = DateTimeParser::parse($row['submitted_at']); + assert($submitted_at instanceof DateTimeImmutable, "Failed to parse " . $row['submitted_at']); + $author_id = (int) $row['author_id']; + + return new Answer( + answer_id: $answer_id, + quiz_id: $quiz_id, + answer_number: $answer_number, + submitted_at: $submitted_at, + author_id: $author_id, + code: $row['code'], + code_size: (int) $row['code_size'], + execution_status: AggregatedExecutionStatus::fromInt((int)$row['execution_status']), + author_name: $row['author_name'] ?? null, + author_is_admin: (bool) ($row['author_is_admin'] ?? null), + ); + } +} diff --git a/services/app/src/Repositories/QuizRepository.php b/services/app/src/Repositories/QuizRepository.php new file mode 100644 index 0000000..b360f9e --- /dev/null +++ b/services/app/src/Repositories/QuizRepository.php @@ -0,0 +1,234 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Repositories; + +use DateTimeImmutable; +use DateTimeZone; +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Models\Quiz; +use Nsfisis\Albatross\Sql\DateTimeParser; +use PDOException; + +final class QuizRepository +{ + private const QUIZ_FIELDS = [ + 'quiz_id', + 'created_at', + 'started_at', + 'ranking_hidden_at', + 'finished_at', + 'title', + 'slug', + 'description', + 'example_code', + 'birdie_code_size', + ]; + + public function __construct( + private readonly Connection $conn, + ) { + } + + /** + * @return Quiz[] + */ + public function listAll(): array + { + $result = $this->conn + ->query() + ->select('quizzes') + ->fields(self::QUIZ_FIELDS) + ->orderBy([['created_at', 'ASC']]) + ->execute(); + return array_map($this->mapRawRowToQuiz(...), $result); + } + + /** + * @return Quiz[] + */ + public function listStarted(?DateTimeImmutable $now = null): array + { + if ($now === null) { + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + } + $result = $this->conn + ->query() + ->select('quizzes') + ->fields(self::QUIZ_FIELDS) + ->where('started_at <= :now') + ->orderBy([['created_at', 'ASC']]) + ->execute(['now' => $now->format('Y-m-d H:i:s.u')]); + return array_map($this->mapRawRowToQuiz(...), $result); + } + + public function findById(int $quiz_id): ?Quiz + { + $result = $this->conn + ->query() + ->select('quizzes') + ->fields(self::QUIZ_FIELDS) + ->where('quiz_id = :quiz_id') + ->first() + ->execute(['quiz_id' => $quiz_id]); + return isset($result) ? $this->mapRawRowToQuiz($result) : null; + } + + public function findBySlug(string $slug): ?Quiz + { + $result = $this->conn + ->query() + ->select('quizzes') + ->fields(self::QUIZ_FIELDS) + ->where('slug = :slug') + ->first() + ->execute(['slug' => $slug]); + return isset($result) ? $this->mapRawRowToQuiz($result) : null; + } + + public function create( + string $title, + string $slug, + string $description, + string $example_code, + ?int $birdie_code_size, + DateTimeImmutable $started_at, + DateTimeImmutable $ranking_hidden_at, + DateTimeImmutable $finished_at, + ): int { + $quiz = Quiz::create( + title: $title, + slug: $slug, + description: $description, + example_code: $example_code, + birdie_code_size: $birdie_code_size, + started_at: $started_at, + ranking_hidden_at: $ranking_hidden_at, + finished_at: $finished_at, + ); + + $values = [ + 'title' => $quiz->title, + 'slug' => $quiz->slug, + 'description' => $quiz->description, + 'example_code' => $quiz->example_code, + 'started_at' => $quiz->started_at->format('Y-m-d H:i:s.u'), + 'ranking_hidden_at' => $quiz->ranking_hidden_at->format('Y-m-d H:i:s.u'), + 'finished_at' => $quiz->finished_at->format('Y-m-d H:i:s.u'), + ]; + if ($quiz->birdie_code_size !== null) { + $values['birdie_code_size'] = $quiz->birdie_code_size; + } + + try { + return $this->conn + ->query() + ->insert('quizzes') + ->values($values) + ->execute(); + } catch (PDOException $e) { + throw new EntityValidationException( + message: '問題の作成に失敗しました', + previous: $e, + ); + } + } + + public function update( + int $quiz_id, + string $title, + string $description, + string $example_code, + ?int $birdie_code_size, + DateTimeImmutable $started_at, + DateTimeImmutable $ranking_hidden_at, + DateTimeImmutable $finished_at, + ): void { + Quiz::validate( + $title, + 'dummy', + $description, + $example_code, + $birdie_code_size, + $started_at, + $ranking_hidden_at, + $finished_at, + ); + + $values = [ + 'title' => $title, + 'description' => $description, + 'example_code' => $example_code, + 'started_at' => $started_at->format('Y-m-d H:i:s.u'), + 'ranking_hidden_at' => $ranking_hidden_at->format('Y-m-d H:i:s.u'), + 'finished_at' => $finished_at->format('Y-m-d H:i:s.u'), + ]; + if ($birdie_code_size !== null) { + $values['birdie_code_size'] = $birdie_code_size; + } + + try { + $this->conn + ->query() + ->update('quizzes') + ->set($values) + ->where('quiz_id = :quiz_id') + ->execute(['quiz_id' => $quiz_id]); + } catch (PDOException $e) { + throw new EntityValidationException( + message: '問題の更新に失敗しました', + previous: $e, + ); + } + } + + public function delete(int $quiz_id): void + { + $this->conn + ->query() + ->delete('quizzes') + ->where('quiz_id = :quiz_id') + ->execute(['quiz_id' => $quiz_id]); + } + + /** + * @param array<string, string> $row + */ + private function mapRawRowToQuiz(array $row): Quiz + { + assert(isset($row['quiz_id'])); + assert(isset($row['created_at'])); + assert(isset($row['started_at'])); + assert(isset($row['ranking_hidden_at'])); + assert(isset($row['finished_at'])); + assert(isset($row['title'])); + assert(isset($row['slug'])); + assert(isset($row['description'])); + assert(isset($row['example_code'])); + + $quiz_id = (int) $row['quiz_id']; + $created_at = DateTimeParser::parse($row['created_at']); + assert($created_at instanceof DateTimeImmutable, "Failed to parse " . $row['created_at']); + $started_at = DateTimeParser::parse($row['started_at']); + assert($started_at instanceof DateTimeImmutable, "Failed to parse " . $row['started_at']); + $ranking_hidden_at = DateTimeParser::parse($row['ranking_hidden_at']); + assert($ranking_hidden_at instanceof DateTimeImmutable, "Failed to parse " . $row['ranking_hidden_at']); + $finished_at = DateTimeParser::parse($row['finished_at']); + assert($finished_at instanceof DateTimeImmutable, "Failed to parse " . $row['finished_at']); + + return new Quiz( + quiz_id: $quiz_id, + created_at: $created_at, + started_at: $started_at, + ranking_hidden_at: $ranking_hidden_at, + finished_at: $finished_at, + title: $row['title'], + slug: $row['slug'], + description: $row['description'], + example_code: $row['example_code'], + birdie_code_size: ($row['birdie_code_size'] ?? '') === '' ? null : (int) $row['birdie_code_size'], + ); + } +} diff --git a/services/app/src/Repositories/TestcaseExecutionRepository.php b/services/app/src/Repositories/TestcaseExecutionRepository.php new file mode 100644 index 0000000..dea0931 --- /dev/null +++ b/services/app/src/Repositories/TestcaseExecutionRepository.php @@ -0,0 +1,296 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Repositories; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Models\ExecutionStatus; +use Nsfisis\Albatross\Models\TestcaseExecution; + +final class TestcaseExecutionRepository +{ + private const TESTCASE_EXECUTION_FIELDS = [ + 'testcase_execution_id', + 'testcase_id', + 'answer_id', + 'status', + 'stdout', + 'stderr', + ]; + + public function __construct( + private readonly Connection $conn, + ) { + } + + public function findByAnswerIdAndTestcaseExecutionId( + int $answer_id, + int $testcase_execution_id, + ): ?TestcaseExecution { + $result = $this->conn + ->query() + ->select('testcase_executions') + ->fields(self::TESTCASE_EXECUTION_FIELDS) + ->where('answer_id = :answer_id AND testcase_execution_id = :testcase_execution_id') + ->first() + ->execute([ + 'answer_id' => $answer_id, + 'testcase_execution_id' => $testcase_execution_id, + ]); + return isset($result) ? $this->mapRawRowToTestcaseExecution($result) : null; + } + + /** + * @return TestcaseExecution[] + */ + public function listByQuizId(int $quiz_id): array + { + $result = $this->conn + ->query() + ->select('testcase_executions') + ->fields(self::TESTCASE_EXECUTION_FIELDS) + ->where('quiz_id = :quiz_id') + ->orderBy([['testcase_execution_id', 'ASC']]) + ->execute(['quiz_id' => $quiz_id]); + return array_map($this->mapRawRowToTestcaseExecution(...), $result); + } + + /** + * @return TestcaseExecution[] + */ + public function listByAnswerId(int $answer_id): array + { + $result = $this->conn + ->query() + ->select('testcase_executions') + ->fields(self::TESTCASE_EXECUTION_FIELDS) + ->where('answer_id = :answer_id') + ->orderBy([['testcase_execution_id', 'ASC']]) + ->execute(['answer_id' => $answer_id]); + return array_map($this->mapRawRowToTestcaseExecution(...), $result); + } + + /** + * @return array<int, ExecutionStatus> + */ + public function getStatuses(int $answer_id): array + { + $result = $this->conn + ->query() + ->select('testcase_executions') + ->fields(['testcase_execution_id', 'status']) + ->where('answer_id = :answer_id') + ->orderBy([['testcase_execution_id', 'ASC']]) + ->execute(['answer_id' => $answer_id]); + return array_combine( + array_map(fn ($row) => (int)$row['testcase_execution_id'], $result), + array_map(fn ($row) => ExecutionStatus::fromInt((int)$row['status']), $result), + ); + } + + public function tryGetNextPendingTestcaseExecution(): ?TestcaseExecution + { + return $this->conn->transaction(function () { + $pending_ex_result = $this->conn + ->query() + ->select('testcase_executions') + ->fields(self::TESTCASE_EXECUTION_FIELDS) + ->where('status = :status') + ->orderBy([['testcase_execution_id', 'ASC']]) + ->first() + ->execute(['status' => ExecutionStatus::Pending->toInt()]); + $pending_ex = isset($pending_ex_result) ? $this->mapRawRowToTestcaseExecution($pending_ex_result) : null; + if ($pending_ex === null) { + return null; + } + $this->conn + ->query() + ->update('testcase_executions') + ->set(['status' => ExecutionStatus::Running->toInt()]) + ->where('testcase_execution_id = :testcase_execution_id') + ->execute(['testcase_execution_id' => $pending_ex->testcase_execution_id]); + return new TestcaseExecution( + testcase_execution_id: $pending_ex->testcase_execution_id, + testcase_id: $pending_ex->testcase_id, + answer_id: $pending_ex->answer_id, + status: ExecutionStatus::Running, + stdout: null, + stderr: null, + ); + }); + } + + public function create( + int $testcase_id, + int $answer_id, + ): int { + $ex = TestcaseExecution::create( + testcase_id: $testcase_id, + answer_id: $answer_id, + ); + + $values = [ + 'testcase_id' => $ex->testcase_id, + 'answer_id' => $ex->answer_id, + 'status' => $ex->status->toInt(), + ]; + + return $this->conn + ->query() + ->insert('testcase_executions') + ->values([ + 'testcase_id' => $ex->testcase_id, + 'answer_id' => $ex->answer_id, + 'status' => $ex->status->toInt(), + ]) + ->execute(); + } + + public function enqueueForAllAnswers( + int $quiz_id, + int $testcase_id, + ): void { + $this->conn + ->query() + ->insertFromSelect('testcase_executions') + ->fields(['testcase_id', 'answer_id', 'status']) + ->from($this->conn + ->query() + ->select('answers') + ->fields([':testcase_id', 'answer_id', ':status']) + ->where('quiz_id = :quiz_id')) + ->execute([ + 'quiz_id' => $quiz_id, + 'testcase_id' => $testcase_id, + 'status' => ExecutionStatus::Pending->toInt(), + ]); + } + + public function enqueueForSingleAnswer( + int $answer_id, + int $quiz_id, + ): void { + $this->conn + ->query() + ->insertFromSelect('testcase_executions') + ->fields(['testcase_id', 'answer_id', 'status']) + ->from($this->conn + ->query() + ->select('testcases') + ->fields(['testcase_id', ':answer_id', ':status']) + ->where('quiz_id = :quiz_id')) + ->execute([ + 'quiz_id' => $quiz_id, + 'answer_id' => $answer_id, + 'status' => ExecutionStatus::Pending->toInt(), + ]); + } + + public function markAllAsPendingByQuizId( + int $quiz_id, + ): void { + $this->conn + ->query() + ->update('testcase_executions') + ->set([ + 'status' => ExecutionStatus::Pending->toInt(), + 'stdout' => '', + 'stderr' => '', + ]) + ->where('answer_id IN (SELECT answer_id FROM answers WHERE quiz_id = :quiz_id)') + ->execute(['quiz_id' => $quiz_id]); + } + + public function markAllAsPendingByAnswerId( + int $answer_id, + ): void { + $this->conn + ->query() + ->update('testcase_executions') + ->set([ + 'status' => ExecutionStatus::Pending->toInt(), + 'stdout' => '', + 'stderr' => '', + ]) + ->where('answer_id = :answer_id') + ->execute(['answer_id' => $answer_id]); + } + + public function markAllAsPendingByTestcaseId( + int $testcase_id, + ): void { + $this->conn + ->query() + ->update('testcase_executions') + ->set([ + 'status' => ExecutionStatus::Pending->toInt(), + 'stdout' => '', + 'stderr' => '', + ]) + ->where('testcase_id = :testcase_id') + ->execute(['testcase_id' => $testcase_id]); + } + + public function markAsPending(int $testcase_execution_id): void + { + $this->update($testcase_execution_id, ExecutionStatus::Pending, '', ''); + } + + public function update( + int $testcase_execution_id, + ExecutionStatus $status, + ?string $stdout, + ?string $stderr, + ): void { + $values = [ + 'status' => $status->toInt(), + ]; + if ($stdout !== null) { + $values['stdout'] = $stdout; + } + if ($stderr !== null) { + $values['stderr'] = $stderr; + } + + $this->conn + ->query() + ->update('testcase_executions') + ->set($values) + ->where('testcase_execution_id = :testcase_execution_id') + ->execute(['testcase_execution_id' => $testcase_execution_id]); + } + + public function deleteByTestcaseId(int $testcase_id): void + { + $this->conn + ->query() + ->delete('testcase_executions') + ->where('testcase_id = :testcase_id') + ->execute(['testcase_id' => $testcase_id]); + } + + /** + * @param array<string, string> $row + */ + private function mapRawRowToTestcaseExecution(array $row): TestcaseExecution + { + assert(isset($row['testcase_execution_id'])); + assert(isset($row['testcase_id'])); + assert(isset($row['answer_id'])); + assert(isset($row['status'])); + + $testcase_execution_id = (int) $row['testcase_execution_id']; + $testcase_id = (int) $row['testcase_id']; + $answer_id = (int) $row['answer_id']; + + return new TestcaseExecution( + testcase_execution_id: $testcase_execution_id, + testcase_id: $testcase_id, + answer_id: $answer_id, + status: ExecutionStatus::fromInt((int)$row['status']), + stdout: $row['stdout'] ?? null, + stderr: $row['stderr'] ?? null, + ); + } +} diff --git a/services/app/src/Repositories/TestcaseRepository.php b/services/app/src/Repositories/TestcaseRepository.php new file mode 100644 index 0000000..4fd3297 --- /dev/null +++ b/services/app/src/Repositories/TestcaseRepository.php @@ -0,0 +1,143 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Repositories; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Models\Testcase; +use PDOException; + +final class TestcaseRepository +{ + private const TESTCASE_FIELDS = [ + 'testcase_id', + 'quiz_id', + 'input', + 'expected_result', + ]; + + public function __construct( + private readonly Connection $conn, + ) { + } + + public function findByQuizIdAndTestcaseId( + int $quiz_id, + int $testcase_id, + ): ?Testcase { + $result = $this->conn + ->query() + ->select('testcases') + ->fields(self::TESTCASE_FIELDS) + ->where('quiz_id = :quiz_id AND testcase_id = :testcase_id') + ->first() + ->execute([ + 'quiz_id' => $quiz_id, + 'testcase_id' => $testcase_id, + ]); + return isset($result) ? $this->mapRawRowToTestcase($result) : null; + } + + /** + * @return Testcase[] + */ + public function listByQuizId(int $quiz_id): array + { + $result = $this->conn + ->query() + ->select('testcases') + ->fields(self::TESTCASE_FIELDS) + ->where('quiz_id = :quiz_id') + ->orderBy([['testcase_id', 'ASC']]) + ->execute(['quiz_id' => $quiz_id]); + return array_map($this->mapRawRowToTestcase(...), $result); + } + + public function create( + int $quiz_id, + string $input, + string $expected_result, + ): int { + $testcase = Testcase::create( + quiz_id: $quiz_id, + input: $input, + expected_result: $expected_result, + ); + + $values = [ + 'quiz_id' => $testcase->quiz_id, + 'input' => $testcase->input, + 'expected_result' => $testcase->expected_result, + ]; + + try { + return $this->conn + ->query() + ->insert('testcases') + ->values($values) + ->execute(); + } catch (PDOException $e) { + throw new EntityValidationException( + message: 'テストケースの作成に失敗しました', + previous: $e, + ); + } + } + + public function update( + int $testcase_id, + string $input, + string $expected_result, + ): void { + try { + $this->conn + ->query() + ->update('testcases') + ->set([ + 'input' => $input, + 'expected_result' => $expected_result, + ]) + ->where('testcase_id = :testcase_id') + ->execute([ + 'testcase_id' => $testcase_id, + ]); + } catch (PDOException $e) { + throw new EntityValidationException( + message: 'テストケースの更新に失敗しました', + previous: $e, + ); + } + } + + public function delete(int $testcase_id): void + { + $this->conn + ->query() + ->delete('testcases') + ->where('testcase_id = :testcase_id') + ->execute(['testcase_id' => $testcase_id]); + } + + /** + * @param array<string, string> $row + */ + private function mapRawRowToTestcase(array $row): Testcase + { + assert(isset($row['testcase_id'])); + assert(isset($row['quiz_id'])); + assert(isset($row['input'])); + assert(isset($row['expected_result'])); + + $testcase_id = (int) $row['testcase_id']; + $quiz_id = (int) $row['quiz_id']; + + return new Testcase( + testcase_id: $testcase_id, + quiz_id: $quiz_id, + input: $row['input'], + expected_result: $row['expected_result'], + ); + } +} diff --git a/services/app/src/Repositories/UserRepository.php b/services/app/src/Repositories/UserRepository.php new file mode 100644 index 0000000..adca397 --- /dev/null +++ b/services/app/src/Repositories/UserRepository.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Albatross\Repositories; + +use Nsfisis\Albatross\Database\Connection; +use Nsfisis\Albatross\Exceptions\EntityValidationException; +use Nsfisis\Albatross\Models\User; +use PDOException; + +final class UserRepository +{ + public function __construct( + private readonly Connection $conn, + ) { + } + + /** + * @return User[] + */ + public function listAll(): array + { + $result = $this->conn + ->query() + ->select('users') + ->fields(['user_id', 'username', 'is_admin']) + ->orderBy([['user_id', 'ASC']]) + ->execute(); + return array_map($this->mapRawRowToUser(...), $result); + } + + public function findById(int $user_id): ?User + { + $result = $this->conn + ->query() + ->select('users') + ->fields(['user_id', 'username', 'is_admin']) + ->where('user_id = :user_id') + ->first() + ->execute(['user_id' => $user_id]); + return isset($result) ? $this->mapRawRowToUser($result) : null; + } + + public function findByUsername(string $username): ?User + { + $result = $this->conn + ->query() + ->select('users') + ->fields(['user_id', 'username', 'is_admin']) + ->where('username = :username') + ->first() + ->execute(['username' => $username]); + return isset($result) ? $this->mapRawRowToUser($result) : null; + } + + /** + * @return positive-int + */ + public function create( + string $username, + bool $is_admin, + ): int { + $user = User::create( + username: $username, + is_admin: $is_admin, + ); + + try { + return $this->conn + ->query() + ->insert('users') + ->values([ + 'username' => $user->username, + 'is_admin' => +$user->is_admin, + ]) + ->execute(); + } catch (PDOException $e) { + throw new EntityValidationException( + message: 'ユーザの作成に失敗しました', + previous: $e, + ); + } + } + + public function update( + int $user_id, + bool $is_admin, + ): void { + $this->conn + ->query() + ->update('users') + ->set([ + 'is_admin' => +$is_admin, + ]) + ->where('user_id = :user_id') + ->execute(['user_id' => $user_id]); + } + + public function delete(int $user_id): void + { + $this->conn + ->query() + ->delete('users') + ->where('user_id = :user_id') + ->execute(['user_id' => $user_id]); + } + + /** + * @param array<string, string> $row + */ + private function mapRawRowToUser(array $row): User + { + assert(isset($row['user_id'])); + assert(isset($row['username'])); + assert(isset($row['is_admin'])); + + $user_id = (int) $row['user_id']; + $is_admin = (bool) $row['is_admin']; + + return new User( + user_id: $user_id, + username: $row['username'], + is_admin: $is_admin, + ); + } +} |
