Skip to main content

ahri_tre_app/
browser_resource_discovery.rs

1//! Browser-safe Dataset and Datafile discovery workflow projection.
2
3use ahri_tre_protocol::{
4    PublicUuid,
5    domain::DomainSummary,
6    refs::{AssetRef, DataFileRef, DatasetRef, StudyRef, TagRef, TagScope, TagSummary, VersionRef},
7    resource_discovery::{
8        BrowserAssetVersionLifecycle, BrowserDataFileAccessRequestReference,
9        BrowserDataFileDiscoveryRecord, BrowserDataFileVersionDiscoveryRecord,
10        BrowserDatasetAccessRequestReference, BrowserDatasetDiscoveryRecord,
11        BrowserDatasetVersionDiscoveryRecord, BrowserResourceAccessCondition,
12        BrowserResourceDiscoveryEmptyState, BrowserResourceDiscoveryResponse,
13    },
14    study::BrowserContentSummaryPolicy,
15};
16use std::fmt;
17use thiserror::Error;
18
19pub const BROWSER_RESOURCE_DISCOVERY_AUTHORITY: &str = "tre_browser.resource_discovery";
20
21#[derive(Debug, Error, Clone, PartialEq, Eq)]
22pub enum BrowserResourceDiscoveryError {
23    #[error("selected datastore identity is invalid")]
24    InvalidDatastoreIdentity,
25    #[error("selected datastore is not available for browser discovery")]
26    DatastoreUnavailable,
27    #[error("browser resource discovery authority is unavailable")]
28    AuthorityUnavailable,
29    #[error("study was not found in the selected datastore")]
30    StudyNotFound,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct BrowserResourceDiscoverySource {
35    pub datastore_id: PublicUuid,
36    pub study: ahri_tre_pgmeta::PgStudyReadModel,
37    pub study_tags: Vec<ahri_tre_pgmeta::PgTagReadModel>,
38    pub study_access_conditions: Vec<ahri_tre_pgmeta::PgDuoRestrictionReadModel>,
39    pub domains: Vec<ahri_tre_pgmeta::PgDomainReadModel>,
40    pub datasets: Vec<BrowserDatasetDiscoverySourceRecord>,
41    pub datafiles: Vec<BrowserDataFileDiscoverySourceRecord>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct BrowserDatasetDiscoverySourceRecord {
46    pub asset: ahri_tre_pgmeta::PgAssetReadModel,
47    pub tags: Vec<ahri_tre_pgmeta::PgTagReadModel>,
48    pub versions: Vec<BrowserDatasetVersionDiscoverySourceRecord>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct BrowserDatasetVersionDiscoverySourceRecord {
53    pub version: ahri_tre_pgmeta::PgAssetVersionReadModel,
54    pub dataset: ahri_tre_pgmeta::PgDatasetReadModel,
55    pub withdrawn_at: Option<chrono::DateTime<chrono::Utc>>,
56    pub tags: Vec<ahri_tre_pgmeta::PgTagReadModel>,
57    pub access_conditions: Vec<ahri_tre_pgmeta::PgAssetVersionDuoRestrictionReadModel>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct BrowserDataFileDiscoverySourceRecord {
62    pub asset: ahri_tre_pgmeta::PgAssetReadModel,
63    pub tags: Vec<ahri_tre_pgmeta::PgTagReadModel>,
64    pub versions: Vec<BrowserDataFileVersionDiscoverySourceRecord>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct BrowserDataFileVersionDiscoverySourceRecord {
69    pub version: ahri_tre_pgmeta::PgAssetVersionReadModel,
70    pub datafile: BrowserDataFileMetadataSource,
71    pub tags: Vec<ahri_tre_pgmeta::PgTagReadModel>,
72    pub access_conditions: Vec<ahri_tre_pgmeta::PgAssetVersionDuoRestrictionReadModel>,
73}
74
75/// Narrow Datafile facts allowed to cross from the runtime adapter into the
76/// application workflow. Storage, digest, key, content, and preview facts
77/// cannot be represented here.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct BrowserDataFileMetadataSource {
80    pub datafile_id: ahri_tre_types::VersionId,
81    pub format: String,
82    /// Size in bytes of the managed stored artifact, when known.
83    pub size_bytes: Option<u64>,
84    pub compressed: Option<bool>,
85    pub encrypted: Option<bool>,
86}
87
88/// Resolved adapter capability used by the application-owned resource
89/// discovery and catalogue-search workflows.
90pub trait BrowserResourceDiscoveryCapability: fmt::Debug + Send + Sync {
91    fn discover_resources(
92        &self,
93        datastore_id: PublicUuid,
94        study_id: PublicUuid,
95    ) -> Result<BrowserResourceDiscoverySource, BrowserResourceDiscoveryError>;
96
97    fn discover_catalogue_search_resources(
98        &self,
99        datastore_id: PublicUuid,
100    ) -> Result<Vec<BrowserResourceDiscoverySource>, BrowserResourceDiscoveryError>;
101}
102
103pub fn discover_browser_resources(
104    capability: &dyn BrowserResourceDiscoveryCapability,
105    datastore_id: PublicUuid,
106    study_id: PublicUuid,
107) -> Result<BrowserResourceDiscoveryResponse, BrowserResourceDiscoveryError> {
108    capability
109        .discover_resources(datastore_id, study_id)
110        .map(browser_resource_discovery_response)
111}
112
113pub fn browser_resource_discovery_response(
114    source: BrowserResourceDiscoverySource,
115) -> BrowserResourceDiscoveryResponse {
116    let datastore_id = source.datastore_id;
117    let study = StudyRef {
118        datastore_id,
119        kind: ahri_tre_protocol::refs::ObjectKind::Study,
120        id: PublicUuid::from_uuid(source.study.study_id.0),
121    };
122    let domains = source
123        .domains
124        .into_iter()
125        .map(|value| domain_summary(datastore_id, value))
126        .collect::<Vec<_>>();
127    let datasets = source
128        .datasets
129        .into_iter()
130        .map(|record| dataset_record(datastore_id, study, &domains, record))
131        .collect::<Vec<_>>();
132    let datafiles = source
133        .datafiles
134        .into_iter()
135        .map(|record| datafile_record(datastore_id, study, &domains, record))
136        .collect::<Vec<_>>();
137
138    BrowserResourceDiscoveryResponse {
139        authority: BROWSER_RESOURCE_DISCOVERY_AUTHORITY.to_string(),
140        datastore_id,
141        study,
142        domains,
143        datasets_empty_state: datasets
144            .is_empty()
145            .then(|| BrowserResourceDiscoveryEmptyState {
146                code: "no_browser_visible_datasets".to_string(),
147                message: "No browser-visible datasets are available in this study.".to_string(),
148            }),
149        datafiles_empty_state: datafiles
150            .is_empty()
151            .then(|| BrowserResourceDiscoveryEmptyState {
152                code: "no_browser_visible_datafiles".to_string(),
153                message: "No browser-visible datafiles are available in this study.".to_string(),
154            }),
155        datasets,
156        datafiles,
157        content_derived_summaries: BrowserContentSummaryPolicy {
158            included: false,
159            reason:
160                "Content-derived summaries are excluded from broad resource Discovery metadata."
161                    .to_string(),
162        },
163    }
164}
165
166pub fn resource_discovery_failure_response(
167    datastore_id: PublicUuid,
168    study_id: PublicUuid,
169    code: impl Into<String>,
170    message: impl Into<String>,
171) -> BrowserResourceDiscoveryResponse {
172    BrowserResourceDiscoveryResponse {
173        authority: BROWSER_RESOURCE_DISCOVERY_AUTHORITY.to_string(),
174        datastore_id,
175        study: StudyRef {
176            datastore_id,
177            kind: ahri_tre_protocol::refs::ObjectKind::Study,
178            id: study_id,
179        },
180        domains: Vec::new(),
181        datasets: Vec::new(),
182        datafiles: Vec::new(),
183        datasets_empty_state: Some(BrowserResourceDiscoveryEmptyState {
184            code: code.into(),
185            message: message.into(),
186        }),
187        datafiles_empty_state: None,
188        content_derived_summaries: BrowserContentSummaryPolicy {
189            included: false,
190            reason:
191                "Content-derived summaries are excluded from broad resource Discovery metadata."
192                    .to_string(),
193        },
194    }
195}
196
197pub(crate) fn dataset_record(
198    datastore_id: PublicUuid,
199    study: StudyRef,
200    domains: &[DomainSummary],
201    record: BrowserDatasetDiscoverySourceRecord,
202) -> BrowserDatasetDiscoveryRecord {
203    let asset = AssetRef {
204        datastore_id,
205        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
206        id: PublicUuid::from_uuid(record.asset.asset_id.0),
207    };
208    let _dataset = latest_available_dataset_version(&record)
209        .unwrap_or_else(|| record.versions.last().expect("dataset has versions"));
210    let dataset_ref = DatasetRef {
211        datastore_id,
212        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
213        id: PublicUuid::from_uuid(record.asset.asset_id.0),
214    };
215    let versions = record
216        .versions
217        .into_iter()
218        .map(|version| dataset_version_record(datastore_id, study, version))
219        .collect();
220
221    BrowserDatasetDiscoveryRecord {
222        asset,
223        dataset: dataset_ref,
224        study,
225        name: record.asset.name,
226        description: record.asset.description,
227        documentation: None,
228        domains: domains.to_vec(),
229        tags: record
230            .tags
231            .into_iter()
232            .map(|tag| tag_summary(datastore_id, tag))
233            .collect(),
234        versions,
235        access_request: BrowserDatasetAccessRequestReference {
236            datastore_id,
237            study,
238            dataset: dataset_ref,
239            version: None,
240        },
241    }
242}
243
244pub(crate) fn latest_available_dataset_version(
245    record: &BrowserDatasetDiscoverySourceRecord,
246) -> Option<&BrowserDatasetVersionDiscoverySourceRecord> {
247    record
248        .versions
249        .iter()
250        .find(|candidate| {
251            candidate.withdrawn_at.is_none() && candidate.version.is_latest.unwrap_or(false)
252        })
253        .or_else(|| {
254            record
255                .versions
256                .iter()
257                .rev()
258                .find(|candidate| candidate.withdrawn_at.is_none())
259        })
260}
261
262fn dataset_version_record(
263    datastore_id: PublicUuid,
264    study: StudyRef,
265    record: BrowserDatasetVersionDiscoverySourceRecord,
266) -> BrowserDatasetVersionDiscoveryRecord {
267    let asset = AssetRef {
268        datastore_id,
269        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
270        id: PublicUuid::from_uuid(record.version.asset_id.0),
271    };
272    let dataset = DatasetRef {
273        datastore_id,
274        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
275        id: PublicUuid::from_uuid(record.version.asset_id.0),
276    };
277    let version = VersionRef {
278        datastore_id,
279        kind: ahri_tre_protocol::refs::ObjectKind::AssetVersion,
280        id: PublicUuid::from_uuid(record.version.version_id.0),
281    };
282    BrowserDatasetVersionDiscoveryRecord {
283        asset,
284        dataset,
285        version,
286        major: record.version.major,
287        minor: record.version.minor,
288        patch: record.version.patch,
289        version_label: record.version.version_label,
290        version_note: record.version.version_note,
291        doi: record.version.doi,
292        is_latest: if record.withdrawn_at.is_some() {
293            Some(false)
294        } else {
295            record.version.is_latest
296        },
297        created_at: record.version.created_at,
298        lifecycle: asset_version_lifecycle(record.withdrawn_at),
299        tags: record
300            .tags
301            .into_iter()
302            .map(|tag| tag_summary(datastore_id, tag))
303            .collect(),
304        access_conditions: record
305            .access_conditions
306            .into_iter()
307            .map(access_condition)
308            .collect(),
309        access_request: BrowserDatasetAccessRequestReference {
310            datastore_id,
311            study,
312            dataset,
313            version: Some(version),
314        },
315    }
316}
317
318pub(crate) fn datafile_record(
319    datastore_id: PublicUuid,
320    study: StudyRef,
321    domains: &[DomainSummary],
322    record: BrowserDataFileDiscoverySourceRecord,
323) -> BrowserDataFileDiscoveryRecord {
324    let asset = AssetRef {
325        datastore_id,
326        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
327        id: PublicUuid::from_uuid(record.asset.asset_id.0),
328    };
329    let _datafile = record
330        .versions
331        .iter()
332        .find(|candidate| candidate.version.is_latest.unwrap_or(false))
333        .unwrap_or_else(|| record.versions.last().expect("datafile has versions"));
334    let datafile_ref = DataFileRef {
335        datastore_id,
336        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
337        id: PublicUuid::from_uuid(record.asset.asset_id.0),
338    };
339    let versions = record
340        .versions
341        .into_iter()
342        .map(|version| datafile_version_record(datastore_id, study, version))
343        .collect();
344
345    BrowserDataFileDiscoveryRecord {
346        asset,
347        datafile: datafile_ref,
348        study,
349        name: record.asset.name,
350        description: record.asset.description,
351        documentation: None,
352        date_created: record.asset.date_created,
353        domains: domains.to_vec(),
354        tags: record
355            .tags
356            .into_iter()
357            .map(|tag| tag_summary(datastore_id, tag))
358            .collect(),
359        versions,
360        access_request: BrowserDataFileAccessRequestReference {
361            datastore_id,
362            study,
363            datafile: datafile_ref,
364            version: None,
365        },
366    }
367}
368
369fn datafile_version_record(
370    datastore_id: PublicUuid,
371    study: StudyRef,
372    record: BrowserDataFileVersionDiscoverySourceRecord,
373) -> BrowserDataFileVersionDiscoveryRecord {
374    let asset = AssetRef {
375        datastore_id,
376        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
377        id: PublicUuid::from_uuid(record.version.asset_id.0),
378    };
379    let datafile = DataFileRef {
380        datastore_id,
381        kind: ahri_tre_protocol::refs::ObjectKind::Asset,
382        id: PublicUuid::from_uuid(record.version.asset_id.0),
383    };
384    let version = VersionRef {
385        datastore_id,
386        kind: ahri_tre_protocol::refs::ObjectKind::AssetVersion,
387        id: PublicUuid::from_uuid(record.version.version_id.0),
388    };
389    BrowserDataFileVersionDiscoveryRecord {
390        asset,
391        datafile,
392        version,
393        major: record.version.major,
394        minor: record.version.minor,
395        patch: record.version.patch,
396        version_label: record.version.version_label,
397        version_note: record.version.version_note,
398        doi: record.version.doi,
399        is_latest: record.version.is_latest,
400        created_at: record.version.created_at,
401        lifecycle: asset_version_lifecycle(None),
402        format: record.datafile.format,
403        size_bytes: record.datafile.size_bytes,
404        compressed: record.datafile.compressed,
405        encrypted: record.datafile.encrypted,
406        tags: record
407            .tags
408            .into_iter()
409            .map(|tag| tag_summary(datastore_id, tag))
410            .collect(),
411        access_conditions: record
412            .access_conditions
413            .into_iter()
414            .map(access_condition)
415            .collect(),
416        access_request: BrowserDataFileAccessRequestReference {
417            datastore_id,
418            study,
419            datafile,
420            version: Some(version),
421        },
422    }
423}
424
425fn asset_version_lifecycle(
426    withdrawn_at: Option<chrono::DateTime<chrono::Utc>>,
427) -> BrowserAssetVersionLifecycle {
428    match withdrawn_at {
429        Some(withdrawn_at) => BrowserAssetVersionLifecycle::Withdrawn { withdrawn_at },
430        None => BrowserAssetVersionLifecycle::Available,
431    }
432}
433
434fn domain_summary(
435    datastore_id: PublicUuid,
436    domain: ahri_tre_pgmeta::PgDomainReadModel,
437) -> DomainSummary {
438    DomainSummary {
439        domain: crate::projections::domain_ref(datastore_id, domain.domain_id),
440        name: domain.name,
441        description: domain.description,
442        uri: domain.uri,
443    }
444}
445
446fn tag_summary(datastore_id: PublicUuid, tag: ahri_tre_pgmeta::PgTagReadModel) -> TagSummary {
447    TagSummary {
448        tag: TagRef {
449            datastore_id,
450            kind: ahri_tre_protocol::refs::ObjectKind::Tag,
451            id: ahri_tre_protocol::refs::encode_scoped_integer_ref("tag", tag.tag_id),
452        },
453        scope: TagScope::Global,
454        label: tag.name,
455    }
456}
457
458fn access_condition(
459    condition: ahri_tre_pgmeta::PgAssetVersionDuoRestrictionReadModel,
460) -> BrowserResourceAccessCondition {
461    BrowserResourceAccessCondition {
462        duo_id: condition.duo_id,
463        code: condition.duo_code,
464        label: condition.label,
465        qualifier_ontology_namespace: condition.qualifier_ontology_namespace,
466        qualifier_ontology_id: condition.qualifier_ontology_id,
467        qualifier_text: condition.qualifier_text,
468        qualifier_date: condition.qualifier_date,
469        qualifier_integer: condition.qualifier_integer,
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use ahri_tre_types::{AssetId, AssetType, DomainId, StudyId, VersionId};
477    use chrono::{NaiveDate, Utc};
478    use uuid::Uuid;
479
480    #[test]
481    fn projection_includes_current_and_historical_resource_metadata() {
482        let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(42));
483        let response = browser_resource_discovery_response(source());
484
485        assert_eq!(response.authority, BROWSER_RESOURCE_DISCOVERY_AUTHORITY);
486        assert_eq!(response.datastore_id, datastore_id);
487        assert!(!response.content_derived_summaries.included);
488        assert_eq!(response.domains[0].name, "demography");
489        assert_eq!(response.datasets.len(), 1);
490        assert_eq!(response.datafiles.len(), 1);
491
492        let dataset = &response.datasets[0];
493        assert_eq!(dataset.name, "visits");
494        assert_eq!(dataset.description.as_deref(), Some("Visit table"));
495        assert_eq!(dataset.tags[0].label, "analysis-ready");
496        assert_eq!(dataset.versions.len(), 2);
497        assert_eq!(dataset.versions[0].version_label.as_deref(), Some("v1"));
498        assert_eq!(dataset.versions[1].version_label.as_deref(), Some("v2"));
499        assert_eq!(
500            dataset.versions[1].version_note.as_deref(),
501            Some("Second release notes")
502        );
503        assert_eq!(dataset.versions[1].access_conditions[0].code, "GRU");
504        assert!(dataset.versions[1].access_request.version.is_some());
505
506        let datafile = &response.datafiles[0];
507        assert_eq!(datafile.name, "visits.csv");
508        assert_eq!(datafile.versions[0].format, "EDAM:format_3752");
509        assert!(datafile.versions[0].access_request.version.is_some());
510    }
511
512    #[test]
513    fn projection_exposes_browser_safe_lifecycle_and_datafile_detail() {
514        let response = serde_json::to_value(browser_resource_discovery_response(source())).unwrap();
515
516        let dataset_version = &response["datasets"][0]["versions"][0];
517        assert_eq!(
518            dataset_version["asset"]["id"],
519            PublicUuid::from_uuid(Uuid::from_u128(200)).to_string()
520        );
521        assert_eq!(dataset_version["lifecycle"]["status"], "available");
522        assert!(dataset_version["created_at"].is_string());
523
524        let datafile = &response["datafiles"][0];
525        assert!(datafile["date_created"].is_string());
526        let version = &datafile["versions"][0];
527        assert_eq!(
528            version["asset"]["id"],
529            PublicUuid::from_uuid(Uuid::from_u128(300)).to_string()
530        );
531        assert_eq!(version["lifecycle"]["status"], "available");
532        assert!(version["created_at"].is_string());
533        assert_eq!(version["size_bytes"], 1_024);
534        assert_eq!(version["compressed"], true);
535        assert_eq!(version["encrypted"], false);
536    }
537
538    #[test]
539    fn withdrawn_dataset_version_is_explicit_and_never_selected_as_latest_available() {
540        let mut source = source();
541        let withdrawn_at = Utc::now();
542        source.datasets[0].versions[1].withdrawn_at = Some(withdrawn_at);
543
544        let response = browser_resource_discovery_response(source);
545        let dataset = &response.datasets[0];
546        assert_eq!(
547            dataset.dataset.id,
548            PublicUuid::from_uuid(Uuid::from_u128(200))
549        );
550        assert_eq!(
551            dataset.versions[0].version.kind,
552            ahri_tre_protocol::refs::ObjectKind::AssetVersion
553        );
554        assert_eq!(
555            dataset.versions[0].version.id,
556            PublicUuid::from_uuid(Uuid::from_u128(201))
557        );
558        assert_eq!(
559            dataset.versions[1].lifecycle,
560            BrowserAssetVersionLifecycle::Withdrawn { withdrawn_at }
561        );
562        assert_eq!(dataset.versions[1].is_latest, Some(false));
563    }
564
565    #[test]
566    fn projection_has_stable_empty_resource_family_states() {
567        let mut source = source();
568        source.datasets.clear();
569        source.datafiles.clear();
570        let response = browser_resource_discovery_response(source);
571
572        assert_eq!(
573            response.datasets_empty_state.unwrap().code,
574            "no_browser_visible_datasets"
575        );
576        assert_eq!(
577            response.datafiles_empty_state.unwrap().code,
578            "no_browser_visible_datafiles"
579        );
580    }
581
582    #[test]
583    fn projection_excludes_rows_bytes_previews_and_private_internals() {
584        let text = serde_json::to_string(&browser_resource_discovery_response(source()))
585            .unwrap()
586            .to_ascii_lowercase();
587
588        for forbidden in [
589            "agent-secret-instruction",
590            "storage_uri",
591            "file:///srv/ahri-tre/lake/visits.csv",
592            "encryption_key",
593            "secret-key-material",
594            "digest",
595            "sha256",
596            "dataset rows",
597            "datafile bytes",
598            "preview",
599            "postgres://",
600            "password",
601            "credential",
602            "ducklake_catalog",
603            "runtime handle",
604            "select ",
605        ] {
606            assert!(!text.contains(forbidden), "{forbidden}");
607        }
608    }
609
610    fn source() -> BrowserResourceDiscoverySource {
611        let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(42));
612        let study_id = StudyId(Uuid::from_u128(100));
613        let dataset_asset_id = AssetId(Uuid::from_u128(200));
614        let datafile_asset_id = AssetId(Uuid::from_u128(300));
615        let dataset_v1 = VersionId(Uuid::from_u128(201));
616        let dataset_v2 = VersionId(Uuid::from_u128(202));
617        let datafile_v1 = VersionId(Uuid::from_u128(301));
618
619        BrowserResourceDiscoverySource {
620            datastore_id,
621            study: ahri_tre_pgmeta::PgStudyReadModel {
622                study_id,
623                name: "HDSS".to_string(),
624                description: Some("Household surveillance".to_string()),
625                documentation: Some("Study documentation".to_string()),
626                agent_instructions: Some("agent-secret-instruction".to_string()),
627                external_id: None,
628                study_type_id: None,
629                date_created: None,
630                created_by: None,
631            },
632            study_tags: Vec::new(),
633            study_access_conditions: Vec::new(),
634            domains: vec![ahri_tre_pgmeta::PgDomainReadModel {
635                domain_id: DomainId(7),
636                name: "demography".to_string(),
637                uri: None,
638                description: Some("Demographic metadata".to_string()),
639            }],
640            datasets: vec![BrowserDatasetDiscoverySourceRecord {
641                asset: asset(dataset_asset_id, study_id, "visits", AssetType::Dataset),
642                tags: vec![tag(9, "analysis-ready")],
643                versions: vec![
644                    dataset_version(dataset_asset_id, dataset_v1, 1, "v1", false),
645                    dataset_version(dataset_asset_id, dataset_v2, 2, "v2", true),
646                ],
647            }],
648            datafiles: vec![BrowserDataFileDiscoverySourceRecord {
649                asset: asset(datafile_asset_id, study_id, "visits.csv", AssetType::File),
650                tags: vec![tag(10, "source-file")],
651                versions: vec![datafile_version(datafile_asset_id, datafile_v1)],
652            }],
653        }
654    }
655
656    fn asset(
657        asset_id: AssetId,
658        study_id: StudyId,
659        name: &str,
660        asset_type: AssetType,
661    ) -> ahri_tre_pgmeta::PgAssetReadModel {
662        ahri_tre_pgmeta::PgAssetReadModel {
663            asset_id,
664            study_id,
665            name: name.to_string(),
666            description: Some(
667                if asset_type == AssetType::Dataset {
668                    "Visit table"
669                } else {
670                    "Visit file"
671                }
672                .to_string(),
673            ),
674            agent_instructions: Some("agent-secret-instruction".to_string()),
675            asset_type,
676            date_created: Some(Utc::now()),
677            created_by: Some("custodian@example.org".to_string()),
678        }
679    }
680
681    fn dataset_version(
682        asset_id: AssetId,
683        version_id: VersionId,
684        major: i32,
685        label: &str,
686        latest: bool,
687    ) -> BrowserDatasetVersionDiscoverySourceRecord {
688        BrowserDatasetVersionDiscoverySourceRecord {
689            version: version(asset_id, version_id, major, label, latest),
690            dataset: ahri_tre_pgmeta::PgDatasetReadModel {
691                dataset_id: version_id,
692            },
693            withdrawn_at: None,
694            tags: vec![tag(11, label)],
695            access_conditions: vec![access_condition_model(version_id)],
696        }
697    }
698
699    fn datafile_version(
700        asset_id: AssetId,
701        version_id: VersionId,
702    ) -> BrowserDataFileVersionDiscoverySourceRecord {
703        BrowserDataFileVersionDiscoverySourceRecord {
704            version: version(asset_id, version_id, 1, "raw", true),
705            datafile: BrowserDataFileMetadataSource {
706                datafile_id: version_id,
707                format: "EDAM:format_3752".to_string(),
708                size_bytes: Some(1_024),
709                compressed: Some(true),
710                encrypted: Some(false),
711            },
712            tags: vec![tag(12, "csv")],
713            access_conditions: vec![access_condition_model(version_id)],
714        }
715    }
716
717    fn version(
718        asset_id: AssetId,
719        version_id: VersionId,
720        major: i32,
721        label: &str,
722        latest: bool,
723    ) -> ahri_tre_pgmeta::PgAssetVersionReadModel {
724        ahri_tre_pgmeta::PgAssetVersionReadModel {
725            version_id,
726            asset_id,
727            major,
728            minor: 0,
729            patch: 0,
730            version_label: Some(label.to_string()),
731            version_note: Some(
732                if latest {
733                    "Second release notes"
734                } else {
735                    "First release notes"
736                }
737                .to_string(),
738            ),
739            is_latest: Some(latest),
740            doi: Some(format!("10.0000/{label}")),
741            created_at: Some(Utc::now()),
742            created_by: Some("custodian@example.org".to_string()),
743        }
744    }
745
746    fn tag(tag_id: i64, name: &str) -> ahri_tre_pgmeta::PgTagReadModel {
747        ahri_tre_pgmeta::PgTagReadModel {
748            tag_id,
749            name: name.to_string(),
750        }
751    }
752
753    fn access_condition_model(
754        version_id: VersionId,
755    ) -> ahri_tre_pgmeta::PgAssetVersionDuoRestrictionReadModel {
756        ahri_tre_pgmeta::PgAssetVersionDuoRestrictionReadModel {
757            restriction_id: 1,
758            version_id,
759            duo_id: "DUO:0000042".to_string(),
760            duo_code: "GRU".to_string(),
761            label: "general research use".to_string(),
762            qualifier_ontology_namespace: None,
763            qualifier_ontology_id: None,
764            qualifier_text: None,
765            qualifier_date: Some(NaiveDate::from_ymd_opt(2028, 1, 1).unwrap()),
766            qualifier_integer: None,
767        }
768    }
769}