1use 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#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct BrowserDataFileMetadataSource {
80 pub datafile_id: ahri_tre_types::VersionId,
81 pub format: String,
82 pub size_bytes: Option<u64>,
84 pub compressed: Option<bool>,
85 pub encrypted: Option<bool>,
86}
87
88pub 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}