1use super::*;
2use ahri_tre_protocol::{
3 PublicUuid,
4 refs::{ObjectKind, ObjectRef},
5};
6
7impl AppService {
8 pub fn catalogue_datastore_id(&self) -> Result<PublicUuid, AppError> {
10 self.datastore_id
11 .map(PublicUuid::from_uuid)
12 .ok_or_else(|| AppError::Validation("A retained Datastore binding is required".into()))
13 }
14
15 pub fn catalogue_reference_id(
18 &self,
19 reference: ObjectRef,
20 kind: ObjectKind,
21 ) -> Result<PublicUuid, AppError> {
22 if reference.kind != kind {
23 return Err(AppError::Validation(
24 "Object reference has the wrong kind".into(),
25 ));
26 }
27 if reference.datastore_id != self.catalogue_datastore_id()? {
28 return Err(AppError::Conflict(
29 "Object reference belongs to another Datastore".into(),
30 ));
31 }
32 Ok(reference.id)
33 }
34
35 pub fn catalogue_domain_selector(
36 &self,
37 selector: ahri_tre_protocol::domain::DomainSelector,
38 ) -> Result<DomainSelector, AppError> {
39 Ok(match selector {
40 ahri_tre_protocol::domain::DomainSelector::Name { name } => {
41 DomainSelector::Name { name: name.into() }
42 }
43 ahri_tre_protocol::domain::DomainSelector::Id { domain } => DomainSelector::Id {
44 domain_id: ahri_tre_types::DomainId(
45 ahri_tre_protocol::refs::decode_scoped_integer_ref(
46 "domain",
47 self.catalogue_reference_id(domain, ObjectKind::Domain)?,
48 )
49 .ok_or_else(|| AppError::Validation("Invalid Domain reference".into()))?,
50 ),
51 },
52 })
53 }
54
55 pub fn catalogue_study_selector(
56 &self,
57 selector: ahri_tre_protocol::study::StudySelector,
58 ) -> Result<StudySelector, AppError> {
59 Ok(match selector {
60 ahri_tre_protocol::study::StudySelector::Id { study } => StudySelector::Id {
61 study_id: StudyId(
62 self.catalogue_reference_id(study, ObjectKind::Study)?
63 .as_uuid(),
64 ),
65 },
66 ahri_tre_protocol::study::StudySelector::Name { domain, name } => StudySelector::Name {
67 name: name.into(),
68 domain: domain
69 .map(|domain| self.catalogue_domain_selector(domain))
70 .transpose()?,
71 },
72 })
73 }
74
75 pub async fn add_catalogue_study(
76 &self,
77 request: ahri_tre_protocol::study::AddStudyRequest,
78 ) -> Result<StudyAddResult, AppError> {
79 use ahri_tre_protocol::study::StudyTypeSelector;
80 let domain = self.catalogue_domain_selector(request.domain)?;
81 let type_id = match request.study_type {
82 StudyTypeSelector::Reference(reference) => ahri_tre_types::StudyTypeId(
83 ahri_tre_protocol::refs::decode_scoped_integer_ref(
84 "study_type",
85 self.catalogue_reference_id(reference, ObjectKind::StudyType)?,
86 )
87 .ok_or_else(|| AppError::Validation("Invalid Study type reference".into()))?,
88 ),
89 StudyTypeSelector::Symbol(symbol) => {
90 if symbol.trim().parse::<i64>().is_ok() {
91 return Err(AppError::Validation(
92 "Numeric Study type selectors are not supported".into(),
93 ));
94 }
95 parse_study_type_id(&symbol)?
96 }
97 };
98 if !self
99 .studies
100 .study_type_exists(type_id)
101 .await
102 .map_err(|error| core_error("lookup Study type", error))?
103 {
104 return Err(AppError::NotFound("Study type was not found".into()));
105 }
106 self.add_study(AddStudyRequest {
107 name: request.name.into(),
108 external_id: request.external_id,
109 study_type: type_id.0.to_string(),
110 domain,
111 description: request.description,
112 })
113 .await
114 }
115
116 pub async fn resolve_catalogue_asset(
117 &self,
118 selector: ahri_tre_protocol::asset::AssetSelector,
119 ) -> Result<
120 (
121 StudyRecord,
122 AssetVersionCatalog,
123 Option<ahri_tre_types::VersionId>,
124 ),
125 AppError,
126 > {
127 use ahri_tre_protocol::asset::AssetSelector;
128 match &selector {
130 AssetSelector::Id {
131 study: Some(study), ..
132 }
133 | AssetSelector::Name { study, .. } => {
134 self.catalogue_study_selector(study.clone())?;
135 }
136 _ => {}
137 }
138 let (asset, asset_type, constraint, pinned) = match selector {
139 AssetSelector::Id {
140 asset,
141 asset_type,
142 study,
143 } => {
144 let (id, pinned) = if asset.kind == ObjectKind::AssetVersion {
145 let version_id = ahri_tre_types::VersionId(
146 self.catalogue_reference_id(asset, ObjectKind::AssetVersion)?
147 .as_uuid(),
148 );
149 let version = self
150 .assets
151 .get_asset_version(version_id)
152 .await
153 .map_err(|error| core_error("lookup Asset version", error))?
154 .ok_or_else(|| {
155 AppError::NotFound(
156 "Asset version was not found or is not visible".into(),
157 )
158 })?;
159 (version.asset_id, Some(version_id))
160 } else {
161 (
162 ahri_tre_types::AssetId(
163 self.catalogue_reference_id(asset, ObjectKind::Asset)?
164 .as_uuid(),
165 ),
166 None,
167 )
168 };
169 (
170 self.assets
171 .get_asset_by_id(id)
172 .await
173 .map_err(|error| core_error("lookup Asset", error))?,
174 asset_type,
175 study,
176 pinned,
177 )
178 }
179 AssetSelector::Name {
180 study,
181 name,
182 asset_type,
183 } => {
184 let study = self
185 .resolve_study_selector(self.catalogue_study_selector(study)?, "Asset Study")
186 .await?;
187 let name = ahri_tre_types::NcName::parse(name.as_str().to_owned())
188 .map_err(|_| AppError::Validation("Invalid Asset name".into()))?;
189 (
190 self.assets
191 .get_asset_by_name(study.study_id, name.as_str())
192 .await
193 .map_err(|error| core_error("lookup Asset", error))?,
194 asset_type,
195 None,
196 None,
197 )
198 }
199 };
200 let asset = asset
201 .ok_or_else(|| AppError::NotFound("Asset was not found or is not visible".into()))?;
202 let study = self
203 .resolve_study_selector(
204 StudySelector::Id {
205 study_id: asset.study_id,
206 },
207 "Asset Study",
208 )
209 .await?;
210 if let Some(constraint) = constraint {
211 let parent = self
212 .resolve_study_selector(
213 self.catalogue_study_selector(constraint)?,
214 "Asset Study constraint",
215 )
216 .await?;
217 if parent.study_id != study.study_id {
218 return Err(AppError::Conflict("Study selectors disagree".into()));
219 }
220 }
221 if asset_type.is_some_and(|kind| kind != asset.asset_type) {
222 return Err(AppError::Conflict(
223 "Asset subtype disagrees with selector".into(),
224 ));
225 }
226 Ok((study, self.asset_version_catalog(asset).await?, pinned))
227 }
228
229 pub(super) fn catalogue_version(
230 &self,
231 catalog: &AssetVersionCatalog,
232 pinned: Option<ahri_tre_types::VersionId>,
233 selector: Option<&str>,
234 ) -> Result<ahri_tre_types::AssetVersionRecord, AppError> {
235 let pinned = pinned
236 .map(|id| {
237 catalog
238 .versions
239 .iter()
240 .find(|version| version.version_id == id)
241 .cloned()
242 .ok_or_else(|| AppError::NotFound("Asset version is not available".into()))
243 })
244 .transpose()?;
245 if let (Some(pinned), None) = (&pinned, selector) {
246 return Ok(pinned.clone());
247 }
248 let text = selector.map(str::trim).unwrap_or("latest");
249 if text.is_empty()
250 || (!text.eq_ignore_ascii_case("latest") && parse_semver_selector(text).is_none())
251 {
252 return Err(AppError::Validation("Invalid version selector".into()));
253 }
254 let selected = resolve_requested_asset_version(catalog, Some(text))
255 .map_err(|_| AppError::NotFound("Asset version is not available".into()))?;
256 if pinned.is_some_and(|version| version.version_id != selected.version_id) {
257 return Err(AppError::Conflict("Version selectors disagree".into()));
258 }
259 Ok(selected)
260 }
261
262 pub async fn list_catalogue_datasets(
264 &self,
265 study: ahri_tre_protocol::study::StudySelector,
266 include_versions: bool,
267 ) -> Result<(StudyRecord, crate::StudyDatasetList), AppError> {
268 let study = self
269 .resolve_study_selector(self.catalogue_study_selector(study)?, "Dataset Study")
270 .await?;
271 let listing = self
272 .list_study_datasets(ListStudyDatasetsRequest {
273 study_id: study.study_id,
274 include_versions,
275 })
276 .await?;
277 Ok((study, listing))
278 }
279 pub async fn list_catalogue_assets(
280 &self,
281 selector: ahri_tre_protocol::study::StudySelector,
282 asset_type: Option<ahri_tre_types::AssetType>,
283 ) -> Result<(StudyRecord, Vec<ahri_tre_protocol::asset::SafeAssetSummary>), AppError> {
284 let datastore_id = self.catalogue_datastore_id()?;
285 let study = self
286 .resolve_study_selector(self.catalogue_study_selector(selector)?, "Asset Study")
287 .await?;
288 let assets = self
289 .list_study_assets(ListStudyAssetsRequest {
290 study_id: study.study_id,
291 })
292 .await?;
293 let mut result = Vec::new();
294 for asset in assets
295 .into_iter()
296 .filter(|asset| asset_type.is_none_or(|kind| kind == asset.asset_type))
297 {
298 result.push(
299 crate::projections::safe_asset_catalog(
300 datastore_id,
301 AssetVersionCatalog {
302 asset,
303 versions: vec![],
304 latest_version: None,
305 },
306 )
307 .asset,
308 );
309 }
310 Ok((study, result))
311 }
312
313 pub async fn read_catalogue_datafile(
314 &self,
315 selector: ahri_tre_protocol::datafile::DataFileSelector,
316 version: Option<&str>,
317 ) -> Result<(StudyRecord, DataFileMetadata), AppError> {
318 use ahri_tre_protocol::{asset::AssetSelector, datafile::DataFileSelector};
319 let selector = match selector {
320 DataFileSelector::Id { datafile, study } => AssetSelector::Id {
321 asset: datafile,
322 study,
323 asset_type: Some(ahri_tre_types::AssetType::File),
324 },
325 DataFileSelector::Name { study, name } => AssetSelector::Name {
326 study,
327 name,
328 asset_type: Some(ahri_tre_types::AssetType::File),
329 },
330 };
331 let (study, catalog, pinned) = self.resolve_catalogue_asset(selector).await?;
332 let selected = self.catalogue_version(&catalog, pinned, version)?;
333 let datafile = self
334 .assets
335 .get_datafile(selected.version_id)
336 .await
337 .map_err(|error| core_error("read Datafile metadata", error))?
338 .ok_or_else(|| AppError::NotFound("Datafile version was not found".into()))?;
339 Ok((
340 study,
341 DataFileMetadata {
342 catalog,
343 version: selected,
344 datafile,
345 },
346 ))
347 }
348
349 pub async fn read_catalogue_dataset(
350 &self,
351 selector: ahri_tre_protocol::dataset::DatasetSelector,
352 with_variables: bool,
353 ) -> Result<DatasetMetadataResult, AppError> {
354 use ahri_tre_protocol::{asset::AssetSelector, dataset::DatasetSelector};
355 let selector = match selector {
356 DatasetSelector::Id { dataset, study } => AssetSelector::Id {
357 asset: dataset,
358 study,
359 asset_type: Some(ahri_tre_types::AssetType::Dataset),
360 },
361 DatasetSelector::Name { study, name } => AssetSelector::Name {
362 study,
363 name,
364 asset_type: Some(ahri_tre_types::AssetType::Dataset),
365 },
366 };
367 let (study, catalog, pinned) = self.resolve_catalogue_asset(selector).await?;
368 let catalog = self.complete_dataset_catalog(catalog.asset).await?;
369 let selected = self.catalogue_version(&catalog, pinned, None)?;
370 let dataset = self
371 .assets
372 .get_dataset(selected.version_id)
373 .await
374 .map_err(|error| core_error("read Dataset metadata", error))?
375 .ok_or_else(|| AppError::NotFound("Dataset version was not found".into()))?;
376 let links = self
377 .variables
378 .list_dataset_variables(dataset.dataset_id)
379 .await
380 .map_err(|error| core_error("list Dataset variables", error))?;
381 let variable_count = links.len();
382 let mut variables = Vec::new();
383 if with_variables {
384 for link in links {
385 variables.push(self.dataset_variable_metadata(link).await?);
386 }
387 }
388 Ok(DatasetMetadataResult {
389 study,
390 metadata: StudyDatasetMetadata {
391 catalog,
392 dataset,
393 variables_included: with_variables,
394 variable_count,
395 variables,
396 },
397 })
398 }
399}