1use chrono::{DateTime, Utc};
2use serde_json::Value;
3use sqlx::{PgPool, Row};
4
5use agentics_domain::models::auth::GithubUserId;
6use agentics_domain::models::challenge_creation::{
7 ChallengeCreationManifest, ChallengeCreationRequestKind, ChallengePrivateAssetKind,
8 ChallengePrivateAssetStatus, ChallengeReviewRecordStatus, ChallengeReviewValidationStatus,
9};
10use agentics_domain::models::github::GithubPullRequestNumber;
11use agentics_domain::models::hashes::{GitCommitSha, Sha256Digest};
12use agentics_domain::models::ids::{
13 ChallengePrivateAssetId, ChallengeReviewRecordId, ChallengeReviewValidationRecordId, HumanId,
14};
15use agentics_domain::models::names::{AssetName, ChallengeName};
16use agentics_domain::models::paths::RepoRelativePath;
17use agentics_domain::models::urls::{GithubPullRequestUrl, GithubRepoRemote};
18use agentics_domain::storage::StorageKey;
19use agentics_error::{Result, ServiceError};
20
21use super::super::ids::{
22 asset_name_from_row, challenge_name_from_row, challenge_private_asset_id_from_row,
23 challenge_review_record_id_from_row, challenge_review_validation_record_id_from_row,
24 human_id_from_row, optional_challenge_name_from_row,
25};
26
27#[derive(Debug, Clone)]
29pub struct ChallengeReviewValidationRecord {
30 pub id: ChallengeReviewValidationRecordId,
31 pub review_record_id: ChallengeReviewRecordId,
32 pub status: ChallengeReviewValidationStatus,
33 pub message: String,
34 pub repository_path: String,
35 pub manifest_sha256: Sha256Digest,
36 pub bundle_sha256: Option<Sha256Digest>,
37 pub created_at: DateTime<Utc>,
38}
39
40#[derive(Debug, Clone)]
42pub struct ChallengePrivateAssetRecord {
43 pub id: ChallengePrivateAssetId,
44 pub review_record_id: ChallengeReviewRecordId,
45 pub asset_name: AssetName,
46 pub kind: ChallengePrivateAssetKind,
47 pub required: bool,
48 pub size_bytes: i64,
49 pub sha256: Sha256Digest,
50 pub storage_key: StorageKey,
51 pub uploader_human_id: HumanId,
52 pub created_at: DateTime<Utc>,
53}
54
55#[derive(Debug, Clone)]
57pub struct AdminChallengePrivateAssetRecord {
58 pub id: ChallengePrivateAssetId,
59 pub review_record_id: ChallengeReviewRecordId,
60 pub asset_name: AssetName,
61 pub kind: ChallengePrivateAssetKind,
62 pub required: bool,
63 pub status: ChallengePrivateAssetStatus,
64 pub size_bytes: i64,
65 pub sha256: Sha256Digest,
66 pub storage_key: StorageKey,
67 pub temporary_storage_key: Option<StorageKey>,
68 pub uploader_human_id: HumanId,
69 pub created_at: DateTime<Utc>,
70 pub activated_at: Option<DateTime<Utc>>,
71 pub failed_at: Option<DateTime<Utc>>,
72 pub failure_message: Option<String>,
73}
74
75#[derive(Debug, Clone)]
77pub struct ChallengeReviewRecordRecord {
78 pub id: ChallengeReviewRecordId,
79 pub challenge_name: ChallengeName,
80 pub request: ChallengeCreationRequestKind,
81 pub status: ChallengeReviewRecordStatus,
82 pub creator_human_id: HumanId,
83 pub creator_github_user_id: GithubUserId,
84 pub creator_github_login: String,
85 pub repo_url: GithubRepoRemote,
86 pub pr_number: GithubPullRequestNumber,
87 pub pr_url: GithubPullRequestUrl,
88 pub commit_sha: GitCommitSha,
89 pub challenge_path: RepoRelativePath,
90 pub manifest_sha256: Sha256Digest,
91 pub manifest: ChallengeCreationManifest,
92 pub validation_bundle_sha256: Option<Sha256Digest>,
93 pub approved_bundle_sha256: Option<Sha256Digest>,
94 pub validation_message: Option<String>,
95 pub validation_repository_path: Option<String>,
96 pub published_challenge_name: Option<ChallengeName>,
97 pub private_assets: Vec<ChallengePrivateAssetRecord>,
98 pub validation_records: Vec<ChallengeReviewValidationRecord>,
99 pub created_at: DateTime<Utc>,
100 pub updated_at: DateTime<Utc>,
101}
102
103pub async fn list_challenge_private_asset_states(
105 pool: &PgPool,
106 review_record_id: &ChallengeReviewRecordId,
107) -> Result<Vec<AdminChallengePrivateAssetRecord>> {
108 let rows = sqlx::query(
109 r#"
110 SELECT *
111 FROM challenge_private_assets
112 WHERE review_record_id = $1::uuid
113 ORDER BY created_at ASC
114 "#,
115 )
116 .bind(review_record_id.as_str())
117 .fetch_all(pool)
118 .await?;
119
120 rows.into_iter()
121 .map(row_to_admin_private_asset_record)
122 .collect()
123}
124
125pub(super) async fn list_private_assets_for_review_record(
127 pool: &PgPool,
128 review_record_id: &ChallengeReviewRecordId,
129) -> Result<Vec<ChallengePrivateAssetRecord>> {
130 let rows = sqlx::query(
131 r#"
132 SELECT *
133 FROM challenge_private_assets
134 WHERE review_record_id = $1::uuid
135 AND status = 'active'
136 ORDER BY created_at ASC
137 "#,
138 )
139 .bind(review_record_id.as_str())
140 .fetch_all(pool)
141 .await?;
142
143 rows.into_iter().map(row_to_private_asset_record).collect()
144}
145
146pub(super) async fn list_validation_records_for_review_record(
148 pool: &PgPool,
149 review_record_id: &ChallengeReviewRecordId,
150) -> Result<Vec<ChallengeReviewValidationRecord>> {
151 let rows = sqlx::query(
152 r#"
153 SELECT *
154 FROM challenge_review_validation_records
155 WHERE review_record_id = $1::uuid
156 ORDER BY created_at DESC
157 "#,
158 )
159 .bind(review_record_id.as_str())
160 .fetch_all(pool)
161 .await?;
162
163 rows.into_iter().map(row_to_validation_record).collect()
164}
165
166pub(super) fn row_to_review_record(
168 row: sqlx::postgres::PgRow,
169 private_assets: Vec<ChallengePrivateAssetRecord>,
170 validation_records: Vec<ChallengeReviewValidationRecord>,
171) -> Result<ChallengeReviewRecordRecord> {
172 let manifest_json: Value = row.try_get("manifest_json")?;
173 let manifest: ChallengeCreationManifest =
174 serde_json::from_value(manifest_json).map_err(|e| ServiceError::Internal(e.to_string()))?;
175 let published_challenge_name =
176 optional_challenge_name_from_row(&row, "published_challenge_name")?;
177
178 Ok(ChallengeReviewRecordRecord {
179 id: challenge_review_record_id_from_row(&row, "id")?,
180 challenge_name: challenge_name_from_row(&row, "challenge_name")?,
181 request: request_kind_from_row(&row, "request_kind")?,
182 status: review_record_status_from_row(&row, "status")?,
183 creator_human_id: human_id_from_row(&row, "creator_human_id")?,
184 creator_github_user_id: github_user_id_from_row(&row, "creator_github_user_id")?,
185 creator_github_login: row.try_get("creator_github_login")?,
186 repo_url: github_repo_remote_from_row(&row, "repo_url")?,
187 pr_number: github_pull_request_number_from_row(&row, "pr_number")?,
188 pr_url: github_pull_request_url_from_row(&row, "pr_url")?,
189 commit_sha: git_commit_sha_from_row(&row, "commit_sha")?,
190 challenge_path: repo_relative_path_from_row(&row, "challenge_path")?,
191 manifest_sha256: sha256_digest_from_row(&row, "manifest_sha256")?,
192 manifest,
193 validation_bundle_sha256: optional_sha256_digest_from_row(
194 &row,
195 "validation_bundle_sha256",
196 )?,
197 approved_bundle_sha256: optional_sha256_digest_from_row(&row, "approved_bundle_sha256")?,
198 validation_message: row.try_get("validation_message")?,
199 validation_repository_path: row.try_get("validation_repository_path")?,
200 published_challenge_name,
201 private_assets,
202 validation_records,
203 created_at: row.try_get("created_at")?,
204 updated_at: row.try_get("updated_at")?,
205 })
206}
207
208fn github_user_id_from_row(row: &sqlx::postgres::PgRow, field: &str) -> Result<GithubUserId> {
209 let value = row.try_get::<i64, _>(field)?;
210 GithubUserId::try_new(value)
211 .map_err(|e| ServiceError::Internal(format!("stored invalid GitHub user id: {e}")))
212}
213
214pub(super) fn row_to_private_asset_record(
216 row: sqlx::postgres::PgRow,
217) -> Result<ChallengePrivateAssetRecord> {
218 Ok(ChallengePrivateAssetRecord {
219 id: challenge_private_asset_id_from_row(&row, "id")?,
220 review_record_id: challenge_review_record_id_from_row(&row, "review_record_id")?,
221 asset_name: asset_name_from_row(&row, "asset_name")?,
222 kind: private_asset_kind_from_row(&row, "kind")?,
223 required: row.try_get("required")?,
224 size_bytes: row.try_get("size_bytes")?,
225 sha256: sha256_digest_from_row(&row, "sha256")?,
226 storage_key: storage_key_from_row(&row, "storage_key")?,
227 uploader_human_id: human_id_from_row(&row, "uploader_human_id")?,
228 created_at: row.try_get("created_at")?,
229 })
230}
231
232fn row_to_admin_private_asset_record(
234 row: sqlx::postgres::PgRow,
235) -> Result<AdminChallengePrivateAssetRecord> {
236 Ok(AdminChallengePrivateAssetRecord {
237 id: challenge_private_asset_id_from_row(&row, "id")?,
238 review_record_id: challenge_review_record_id_from_row(&row, "review_record_id")?,
239 asset_name: asset_name_from_row(&row, "asset_name")?,
240 kind: private_asset_kind_from_row(&row, "kind")?,
241 required: row.try_get("required")?,
242 status: private_asset_status_from_row(&row, "status")?,
243 size_bytes: row.try_get("size_bytes")?,
244 sha256: sha256_digest_from_row(&row, "sha256")?,
245 storage_key: storage_key_from_row(&row, "storage_key")?,
246 temporary_storage_key: optional_storage_key_from_row(&row, "temporary_storage_key")?,
247 uploader_human_id: human_id_from_row(&row, "uploader_human_id")?,
248 created_at: row.try_get("created_at")?,
249 activated_at: row.try_get("activated_at")?,
250 failed_at: row.try_get("failed_at")?,
251 failure_message: row.try_get("failure_message")?,
252 })
253}
254
255fn github_repo_remote_from_row(
257 row: &sqlx::postgres::PgRow,
258 column: &str,
259) -> Result<GithubRepoRemote> {
260 let value: String = row.try_get(column)?;
261 GithubRepoRemote::try_new(&value)
262 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
263}
264
265fn github_pull_request_url_from_row(
267 row: &sqlx::postgres::PgRow,
268 column: &str,
269) -> Result<GithubPullRequestUrl> {
270 let value: String = row.try_get(column)?;
271 GithubPullRequestUrl::try_new(&value)
272 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
273}
274
275fn github_pull_request_number_from_row(
277 row: &sqlx::postgres::PgRow,
278 column: &str,
279) -> Result<GithubPullRequestNumber> {
280 let value: i32 = row.try_get(column)?;
281 GithubPullRequestNumber::try_new(value.to_string())
282 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
283}
284
285fn git_commit_sha_from_row(row: &sqlx::postgres::PgRow, column: &str) -> Result<GitCommitSha> {
287 let value: String = row.try_get(column)?;
288 GitCommitSha::try_new(&value)
289 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
290}
291
292fn sha256_digest_from_row(row: &sqlx::postgres::PgRow, column: &str) -> Result<Sha256Digest> {
294 let value: String = row.try_get(column)?;
295 Sha256Digest::try_new(&value)
296 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
297}
298
299fn optional_sha256_digest_from_row(
301 row: &sqlx::postgres::PgRow,
302 column: &str,
303) -> Result<Option<Sha256Digest>> {
304 let Some(value) = row.try_get::<Option<String>, _>(column)? else {
305 return Ok(None);
306 };
307 Sha256Digest::try_new(&value)
308 .map(Some)
309 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
310}
311
312pub(super) fn storage_key_from_row(
314 row: &sqlx::postgres::PgRow,
315 column: &str,
316) -> Result<StorageKey> {
317 let value: String = row.try_get(column)?;
318 StorageKey::try_new(&value)
319 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
320}
321
322pub(super) fn optional_storage_key_from_row(
324 row: &sqlx::postgres::PgRow,
325 column: &str,
326) -> Result<Option<StorageKey>> {
327 let Some(value) = row.try_get::<Option<String>, _>(column)? else {
328 return Ok(None);
329 };
330 StorageKey::try_new(&value)
331 .map(Some)
332 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
333}
334
335fn repo_relative_path_from_row(
337 row: &sqlx::postgres::PgRow,
338 column: &str,
339) -> Result<RepoRelativePath> {
340 let value: String = row.try_get(column)?;
341 RepoRelativePath::try_new(&value)
342 .map_err(|e| ServiceError::Internal(format!("invalid stored {column}: {e}")))
343}
344
345pub(super) fn row_to_validation_record(
347 row: sqlx::postgres::PgRow,
348) -> Result<ChallengeReviewValidationRecord> {
349 Ok(ChallengeReviewValidationRecord {
350 id: challenge_review_validation_record_id_from_row(&row, "id")?,
351 review_record_id: challenge_review_record_id_from_row(&row, "review_record_id")?,
352 status: validation_status_from_row(&row, "status")?,
353 message: row.try_get("message")?,
354 repository_path: row.try_get("repository_path")?,
355 manifest_sha256: sha256_digest_from_row(&row, "manifest_sha256")?,
356 bundle_sha256: optional_sha256_digest_from_row(&row, "bundle_sha256")?,
357 created_at: row.try_get("created_at")?,
358 })
359}
360
361fn request_kind_from_row(
363 row: &sqlx::postgres::PgRow,
364 column: &str,
365) -> Result<ChallengeCreationRequestKind> {
366 let value: String = row.try_get(column)?;
367 ChallengeCreationRequestKind::from_storage_value(&value)
368 .ok_or_else(|| ServiceError::Internal(format!("unknown stored {column} `{value}`")))
369}
370
371pub(super) fn review_record_status_from_row(
373 row: &sqlx::postgres::PgRow,
374 column: &str,
375) -> Result<ChallengeReviewRecordStatus> {
376 let value: String = row.try_get(column)?;
377 ChallengeReviewRecordStatus::from_storage_value(&value)
378 .ok_or_else(|| ServiceError::Internal(format!("unknown stored {column} `{value}`")))
379}
380
381fn validation_status_from_row(
383 row: &sqlx::postgres::PgRow,
384 column: &str,
385) -> Result<ChallengeReviewValidationStatus> {
386 let value: String = row.try_get(column)?;
387 ChallengeReviewValidationStatus::from_storage_value(&value)
388 .ok_or_else(|| ServiceError::Internal(format!("unknown stored {column} `{value}`")))
389}
390
391fn private_asset_status_from_row(
393 row: &sqlx::postgres::PgRow,
394 column: &str,
395) -> Result<ChallengePrivateAssetStatus> {
396 let value: String = row.try_get(column)?;
397 ChallengePrivateAssetStatus::from_storage_value(&value)
398 .ok_or_else(|| ServiceError::Internal(format!("unknown stored {column} `{value}`")))
399}
400
401fn private_asset_kind_from_row(
403 row: &sqlx::postgres::PgRow,
404 column: &str,
405) -> Result<ChallengePrivateAssetKind> {
406 let value: String = row.try_get(column)?;
407 ChallengePrivateAssetKind::from_storage_value(&value)
408 .ok_or_else(|| ServiceError::Internal(format!("unknown stored {column} `{value}`")))
409}