Skip to main content

agentics_persistence/db/challenge_creation/
rows.rs

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/// Review record validation row before DTO projection.
28#[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/// Active private asset row before DTO projection.
41#[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/// Private asset lifecycle row before admin DTO projection.
56#[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/// Challenge review record row plus active assets and validation records before DTO projection.
76#[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
103/// List all private asset lifecycle records for an admin review record review.
104pub 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
125/// Lists active private assets for review_record using the configured query scope.
126pub(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
146/// Lists validation records for review_record using the configured query scope.
147pub(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
166/// Converts a database row into the review_record record model.
167pub(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
214/// Converts a database row into the private asset record model.
215pub(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
232/// Converts a database row into the admin private asset lifecycle record model.
233fn 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
255/// Reads github repo remote from a database row and validates its domain shape.
256fn 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
265/// Reads github pull request url from a database row and validates its domain shape.
266fn 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
275/// Reads github pull request number from a database row and validates its domain shape.
276fn 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
285/// Reads git commit sha from a database row and validates its domain shape.
286fn 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
292/// Reads sha256 digest from a database row and validates its domain shape.
293fn 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
299/// Reads optional sha256 digest from a database row and validates its domain shape.
300fn 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
312/// Reads storage key from a database row and validates its domain shape.
313pub(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
322/// Reads an optional storage key from a database row and validates its domain shape.
323pub(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
335/// Reads repo relative path from a database row and validates its domain shape.
336fn 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
345/// Converts a database row into the validation record model.
346pub(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
361/// Reads request kind from a database row and validates its domain shape.
362fn 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
371/// Reads review_record status from a database row and validates its domain shape.
372pub(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
381/// Reads validation status from a database row and validates its domain shape.
382fn 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
391/// Reads private asset status from a database row and validates its domain shape.
392fn 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
401/// Reads private asset kind from a database row and validates its domain shape.
402fn 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}