1use super::*;
4use ahri_tre_protocol::{
5 dictionary, model,
6 refs::{ObjectKind, ObjectRef},
7};
8
9impl AppService {
10 pub(crate) fn semantic_integer_id(
11 &self,
12 reference: ObjectRef,
13 kind: ObjectKind,
14 scope: &str,
15 ) -> Result<i64, AppError> {
16 let id = self.catalogue_reference_id(reference, kind)?;
17 ahri_tre_protocol::refs::decode_scoped_integer_ref(scope, id)
18 .filter(|id| *id > 0)
19 .ok_or_else(|| AppError::Validation("Invalid semantic reference".into()))
20 }
21
22 pub(super) async fn catalogue_variable_selector(
23 &self,
24 selector: dictionary::VariableSelector,
25 ) -> Result<VariableSelector, AppError> {
26 let (selector, constraint) = match selector {
27 dictionary::VariableSelector::Id { variable, domain } => (
28 VariableSelector::Id {
29 variable_id: ahri_tre_types::VariableId(self.semantic_integer_id(
30 variable,
31 ObjectKind::Variable,
32 "variable",
33 )?),
34 },
35 domain,
36 ),
37 dictionary::VariableSelector::Name { domain, name } => (
38 VariableSelector::Name {
39 domain: self.catalogue_domain_selector(domain)?,
40 name: name.into(),
41 },
42 None,
43 ),
44 };
45 if let Some(domain) = constraint {
46 let domain = self
47 .resolve_domain_selector(
48 self.catalogue_domain_selector(domain)?,
49 "semantic Domain constraint",
50 )
51 .await?;
52 let target = self
53 .resolve_variable_selector_optional(selector.clone())
54 .await?
55 .ok_or_else(|| {
56 AppError::NotFound("Semantic definition was not found or is not visible".into())
57 })?;
58 if target.domain_id != domain.domain_id {
59 return Err(AppError::Conflict(
60 "Semantic definition and Domain selectors disagree".into(),
61 ));
62 }
63 }
64 Ok(selector)
65 }
66
67 pub(super) async fn catalogue_vocabulary_selector(
68 &self,
69 selector: dictionary::VocabularySelector,
70 ) -> Result<VocabularySelector, AppError> {
71 let (selector, constraint) = match selector {
72 dictionary::VocabularySelector::Id { vocabulary, domain } => (
73 VocabularySelector::Id {
74 vocabulary_id: ahri_tre_types::VocabularyId(self.semantic_integer_id(
75 vocabulary,
76 ObjectKind::Vocabulary,
77 "vocabulary",
78 )?),
79 },
80 domain,
81 ),
82 dictionary::VocabularySelector::Name { domain, name } => (
83 VocabularySelector::Name {
84 domain: self.catalogue_domain_selector(domain)?,
85 name: name.into(),
86 },
87 None,
88 ),
89 };
90 if let Some(domain) = constraint {
91 let domain = self
92 .resolve_domain_selector(
93 self.catalogue_domain_selector(domain)?,
94 "semantic Domain constraint",
95 )
96 .await?;
97 let target = self
98 .resolve_vocabulary_selector_optional(selector.clone())
99 .await?
100 .ok_or_else(|| {
101 AppError::NotFound("Semantic definition was not found or is not visible".into())
102 })?;
103 if target.domain_id != domain.domain_id {
104 return Err(AppError::Conflict(
105 "Semantic definition and Domain selectors disagree".into(),
106 ));
107 }
108 }
109 Ok(selector)
110 }
111
112 pub(super) async fn catalogue_entity_selector(
113 &self,
114 selector: model::EntitySelector,
115 ) -> Result<EntitySelector, AppError> {
116 let (selector, constraint) = match selector {
117 model::EntitySelector::Id { entity, domain } => (
118 EntitySelector::Id {
119 entity_id: ahri_tre_types::EntityId(self.semantic_integer_id(
120 entity,
121 ObjectKind::Entity,
122 "entity",
123 )?),
124 },
125 domain,
126 ),
127 model::EntitySelector::Name { domain, name } => (
128 EntitySelector::Name {
129 domain: self.catalogue_domain_selector(domain)?,
130 name: name.into(),
131 },
132 None,
133 ),
134 };
135 if let Some(domain) = constraint {
136 let domain = self
137 .resolve_domain_selector(
138 self.catalogue_domain_selector(domain)?,
139 "semantic Domain constraint",
140 )
141 .await?;
142 let target = self
143 .resolve_entity_selector_optional(selector.clone())
144 .await?
145 .ok_or_else(|| {
146 AppError::NotFound("Semantic definition was not found or is not visible".into())
147 })?;
148 if target.domain_id != domain.domain_id {
149 return Err(AppError::Conflict(
150 "Semantic definition and Domain selectors disagree".into(),
151 ));
152 }
153 }
154 Ok(selector)
155 }
156
157 pub(super) async fn catalogue_relation_selector(
158 &self,
159 selector: model::RelationSelector,
160 ) -> Result<EntityRelationSelector, AppError> {
161 let (selector, constraint) = match selector {
162 model::RelationSelector::Id { relation, domain } => (
163 EntityRelationSelector::Id {
164 entity_relation_id: ahri_tre_types::EntityRelationId(
165 self.semantic_integer_id(relation, ObjectKind::Relation, "relation")?,
166 ),
167 },
168 domain,
169 ),
170 model::RelationSelector::Name { domain, name } => (
171 EntityRelationSelector::Name {
172 domain: self.catalogue_domain_selector(domain)?,
173 name: name.into(),
174 },
175 None,
176 ),
177 };
178 if let Some(domain) = constraint {
179 let domain = self
180 .resolve_domain_selector(
181 self.catalogue_domain_selector(domain)?,
182 "semantic Domain constraint",
183 )
184 .await?;
185 let target = self
186 .resolve_entity_relation_selector_optional(selector.clone())
187 .await?
188 .ok_or_else(|| {
189 AppError::NotFound("Semantic definition was not found or is not visible".into())
190 })?;
191 if target.domain_id != domain.domain_id {
192 return Err(AppError::Conflict(
193 "Semantic definition and Domain selectors disagree".into(),
194 ));
195 }
196 }
197 Ok(selector)
198 }
199
200 pub async fn list_catalogue_variables(
201 &self,
202 request: dictionary::ListVariablesRequest,
203 ) -> Result<dictionary::VariableListResponse, AppError> {
204 let values = self
205 .list_variables(ListVariablesRequest {
206 domain: request
207 .domain
208 .map(|v| self.catalogue_domain_selector(v))
209 .transpose()?,
210 study: request
211 .study
212 .map(|v| self.catalogue_study_selector(v))
213 .transpose()?,
214 })
215 .await?;
216 let mut variables = Vec::with_capacity(values.len());
217 for value in values {
218 variables.push(self.variable_response(value).await?.variable.summary);
219 }
220 Ok(dictionary::VariableListResponse {
221 variables,
222 warnings: None,
223 })
224 }
225
226 pub async fn get_catalogue_variable(
227 &self,
228 request: dictionary::GetVariableRequest,
229 ) -> Result<dictionary::VariableDetailResponse, AppError> {
230 let variable = self
231 .get_variable(GetVariableRequest {
232 variable: self.catalogue_variable_selector(request.variable).await?,
233 })
234 .await?
235 .ok_or_else(|| AppError::NotFound("Variable was not found or is not visible".into()))?;
236 self.variable_response(variable).await
237 }
238
239 pub async fn add_catalogue_variable(
240 &self,
241 request: dictionary::AddVariableRequest,
242 ) -> Result<dictionary::VariableDetailResponse, AppError> {
243 let variable = self
244 .add_variable(AddVariableRequest {
245 domain: self.catalogue_domain_selector(request.domain)?,
246 name: request.name.into(),
247 value_type: request.value_type,
248 value_format: request.value_format,
249 key_role: request.key_role,
250 description: request.description,
251 ontology_namespace: request.ontology_namespace,
252 ontology_class: request.ontology_class,
253 vocabulary: request.vocabulary.map(Into::into),
254 vocabulary_items: request.vocabulary_items,
255 })
256 .await?;
257 self.variable_response(variable).await
258 }
259
260 pub async fn update_catalogue_variable(
261 &self,
262 request: dictionary::UpdateVariableRequest,
263 ) -> Result<dictionary::VariableDetailResponse, AppError> {
264 let selector = self.catalogue_variable_selector(request.variable).await?;
265 let existing = self
266 .resolve_variable_selector_optional(selector)
267 .await?
268 .ok_or_else(|| AppError::NotFound("Variable was not found or is not visible".into()))?;
269 let variable = self
270 .update_variable(UpdateVariableRequest {
271 variable: VariableSelector::Id {
272 variable_id: existing.variable_id,
273 },
274 value_type: request.value_type,
275 value_format: request.value_format,
276 key_role: request.key_role,
277 description: request.description,
278 ontology_namespace: request.ontology_namespace,
279 ontology_class: request.ontology_class,
280 vocabulary: request.vocabulary.map(Into::into),
281 })
282 .await?;
283 self.variable_response(variable).await
284 }
285
286 async fn variable_response(
287 &self,
288 variable: RegisteredVariable,
289 ) -> Result<dictionary::VariableDetailResponse, AppError> {
290 let tags = self
291 .semantic_target_tags(TagAttachmentTarget::Variable(variable.variable.variable_id))
292 .await?;
293 let items = match variable.vocabulary.as_ref() {
294 Some(vocabulary) => self.declared_vocabulary_items(vocabulary.vocabulary_id),
295 None => Some(Vec::new()),
296 };
297 let mut variable =
298 crate::projections::semantic_variable(self.catalogue_datastore_id()?, variable);
299 if let Some(items) = items {
300 variable.vocabulary_items = items.into_iter().map(schema_item).collect();
301 variable.vocabulary_items_withheld = false;
302 if let Some(vocabulary) = variable.summary.vocabulary.as_mut() {
303 vocabulary.item_count = Some(variable.vocabulary_items.len());
304 }
305 }
306 variable.summary.tags = tags;
307 Ok(dictionary::VariableDetailResponse {
308 variable,
309 warnings: None,
310 })
311 }
312
313 async fn semantic_target_tags(
314 &self,
315 target: TagAttachmentTarget,
316 ) -> Result<Vec<ahri_tre_protocol::refs::TagSummary>, AppError> {
317 let datastore_id = self.catalogue_datastore_id()?;
318 Ok(self
319 .get_tags(GetTagsRequest { target })
320 .await?
321 .tags
322 .into_iter()
323 .map(|tag| crate::projections::tag_summary(datastore_id, tag))
324 .collect())
325 }
326
327 pub async fn list_catalogue_vocabularies(
328 &self,
329 request: dictionary::ListVocabulariesRequest,
330 ) -> Result<dictionary::VocabularyListResponse, AppError> {
331 let values = self
332 .list_vocabularies(ListVocabulariesRequest {
333 domain: request
334 .domain
335 .map(|v| self.catalogue_domain_selector(v))
336 .transpose()?,
337 study: request
338 .study
339 .map(|v| self.catalogue_study_selector(v))
340 .transpose()?,
341 })
342 .await?;
343 Ok(dictionary::VocabularyListResponse {
344 vocabularies: values
345 .into_iter()
346 .map(|v| self.vocabulary_response(v).map(|v| v.vocabulary.summary))
347 .collect::<Result<_, _>>()?,
348 warnings: None,
349 })
350 }
351
352 pub async fn get_catalogue_vocabulary(
353 &self,
354 request: dictionary::GetVocabularyRequest,
355 ) -> Result<dictionary::VocabularyDetailResponse, AppError> {
356 let vocabulary = self
357 .get_vocabulary(GetVocabularyRequest {
358 vocabulary: self
359 .catalogue_vocabulary_selector(request.vocabulary)
360 .await?,
361 })
362 .await?
363 .ok_or_else(|| {
364 AppError::NotFound("Vocabulary was not found or is not visible".into())
365 })?;
366 self.vocabulary_response(vocabulary)
367 }
368
369 pub async fn add_catalogue_vocabulary(
370 &self,
371 request: dictionary::AddVocabularyRequest,
372 ) -> Result<dictionary::VocabularyDetailResponse, AppError> {
373 let domain = self
374 .resolve_domain_selector(
375 self.catalogue_domain_selector(request.domain)?,
376 "vocabulary Domain",
377 )
378 .await?;
379 let declaration = RegisterVocabularyRequest {
380 domain_id: domain.domain_id,
381 vocabulary_id: None,
382 name: parse_vocabulary_name(request.name.into())?,
383 description: request.description,
384 items: parse_vocabulary_item_requests(&request.items)?,
385 force_metadata_updates: false,
386 };
387 let repository = self.semantic_repository.as_ref().ok_or_else(|| {
388 AppError::Infrastructure("Dictionary provenance is unavailable".into())
389 })?;
390 let vocabulary = repository
391 .with_semantic_declarations(|repository| {
392 let repositories =
393 crate::session::ScopedSessionMetadataRepositories::from_repository(Arc::new(
394 repository,
395 ));
396 let service = ScopedAppService::new(repositories.into());
397 futures::FutureExt::now_or_never(service.register_vocabulary(declaration))
398 .ok_or_else(|| {
399 CoreError::Infrastructure(
400 "Dictionary declaration unexpectedly suspended".into(),
401 )
402 })?
403 .map_err(semantic_mutation_error)
404 })
405 .map_err(|error| core_error("declare vocabulary", error))?;
406 self.vocabulary_response(vocabulary)
407 }
408
409 fn vocabulary_response(
410 &self,
411 vocabulary: RegisteredVocabulary,
412 ) -> Result<dictionary::VocabularyDetailResponse, AppError> {
413 let items = self.declared_vocabulary_items(vocabulary.vocabulary.vocabulary_id);
414 let mut vocabulary =
415 crate::projections::semantic_vocabulary(self.catalogue_datastore_id()?, vocabulary);
416 if let Some(items) = items {
417 vocabulary.items = items.into_iter().map(schema_item).collect();
418 vocabulary.summary.item_count = Some(vocabulary.items.len());
419 vocabulary.items_withheld = false;
420 }
421 Ok(dictionary::VocabularyDetailResponse {
422 vocabulary,
423 warnings: None,
424 })
425 }
426
427 pub(super) fn declared_vocabulary_items(
428 &self,
429 id: ahri_tre_types::VocabularyId,
430 ) -> Option<Vec<ahri_tre_types::VocabularyItemRecord>> {
431 self.semantic_repository
434 .as_ref()?
435 .catalogue_vocabulary_items(id)
436 .ok()
437 .flatten()
438 }
439
440 pub async fn list_catalogue_entities(
441 &self,
442 request: model::ListEntitiesRequest,
443 ) -> Result<model::EntityListResponse, AppError> {
444 let records = self
445 .list_entities(ListEntitiesRequest {
446 domain: self.catalogue_domain_selector(request.domain)?,
447 })
448 .await?;
449 let mut values = Vec::with_capacity(records.len());
450 for record in records {
451 values.push(self.entity_response(record).await?.entity.summary);
452 }
453 Ok(model::EntityListResponse {
454 entities: values,
455 next_cursor: None,
456 warnings: None,
457 })
458 }
459 pub async fn get_catalogue_entity(
460 &self,
461 request: model::GetEntityRequest,
462 ) -> Result<model::EntityDetailResponse, AppError> {
463 let record = self
464 .get_entity(GetEntityRequest {
465 entity: self.catalogue_entity_selector(request.entity).await?,
466 })
467 .await?
468 .ok_or_else(|| AppError::NotFound("Entity was not found or is not visible".into()))?;
469 self.entity_response(record).await
470 }
471 pub async fn add_catalogue_entity(
472 &self,
473 request: model::AddEntityRequest,
474 ) -> Result<model::EntityDetailResponse, AppError> {
475 let record = self
476 .add_entity(AddEntityRequest {
477 domain: self.catalogue_domain_selector(request.domain)?,
478 name: request.name.into(),
479 description: request.description,
480 agent_instructions: request.agent_instructions,
481 ontology_namespace: request.ontology_namespace,
482 ontology_class: request.ontology_class,
483 })
484 .await?;
485 self.entity_response(record).await
486 }
487 async fn entity_response(
488 &self,
489 record: ahri_tre_types::EntityRecord,
490 ) -> Result<model::EntityDetailResponse, AppError> {
491 let tags = self
492 .semantic_target_tags(TagAttachmentTarget::Entity(record.entity_id))
493 .await?;
494 let mut entity =
495 crate::projections::semantic_entity(self.catalogue_datastore_id()?, record);
496 entity.summary.tags = tags;
497 Ok(model::EntityDetailResponse {
498 entity,
499 warnings: None,
500 })
501 }
502
503 pub async fn list_catalogue_relations(
504 &self,
505 request: model::ListRelationsRequest,
506 ) -> Result<model::RelationListResponse, AppError> {
507 let records = self
508 .list_entity_relations(ListEntityRelationsRequest {
509 domain: self.catalogue_domain_selector(request.domain)?,
510 })
511 .await?;
512 let mut values = Vec::with_capacity(records.len());
513 for record in records {
514 values.push(self.relation_response(record).await?.relation.summary);
515 }
516 Ok(model::RelationListResponse {
517 relations: values,
518 next_cursor: None,
519 warnings: None,
520 })
521 }
522 pub async fn get_catalogue_relation(
523 &self,
524 request: model::GetRelationRequest,
525 ) -> Result<model::RelationDetailResponse, AppError> {
526 let record = self
527 .get_entity_relation(GetEntityRelationRequest {
528 relation: self.catalogue_relation_selector(request.relation).await?,
529 })
530 .await?
531 .ok_or_else(|| AppError::NotFound("Relation was not found or is not visible".into()))?;
532 self.relation_response(record).await
533 }
534 pub async fn add_catalogue_relation(
535 &self,
536 request: model::AddRelationRequest,
537 ) -> Result<model::RelationDetailResponse, AppError> {
538 let record = self
539 .add_entity_relation(AddEntityRelationRequest {
540 domain: self.catalogue_domain_selector(request.domain)?,
541 name: request.name.into(),
542 description: request.description,
543 agent_instructions: request.agent_instructions,
544 ontology_namespace: request.ontology_namespace,
545 ontology_class: request.ontology_class,
546 subject: request.subject.into(),
547 object: request.object.into(),
548 })
549 .await?;
550 self.relation_response(record).await
551 }
552 async fn relation_response(
553 &self,
554 record: ahri_tre_types::EntityRelationRecord,
555 ) -> Result<model::RelationDetailResponse, AppError> {
556 let mut names = BTreeMap::new();
557 for id in [record.subject_entity_id, record.object_entity_id] {
558 if let Some(entity) = self
559 .get_entity(GetEntityRequest {
560 entity: EntitySelector::Id { entity_id: id },
561 })
562 .await?
563 {
564 names.insert(id.0, entity.name.as_str().to_string());
565 }
566 }
567 let tags = self
568 .semantic_target_tags(TagAttachmentTarget::EntityRelation(
569 record.entity_relation_id,
570 ))
571 .await?;
572 let mut relation =
573 crate::projections::semantic_relation(self.catalogue_datastore_id()?, record, &names);
574 relation.summary.tags = tags;
575 Ok(model::RelationDetailResponse {
576 relation,
577 warnings: None,
578 })
579 }
580
581 pub async fn list_catalogue_transformations(
582 &self,
583 request: ahri_tre_protocol::transformation::TransformationListRequest,
584 ) -> Result<Vec<ahri_tre_protocol::transformation::TransformationSummary>, AppError> {
585 let study = self
586 .resolve_study_selector(
587 self.catalogue_study_selector(request.study)?,
588 "Transformation Study",
589 )
590 .await?;
591 let mut values = self.study_transformations(study.study_id).await?;
592 let mut visible = BTreeMap::new();
595 for value in &values {
596 for version_id in value
597 .inputs
598 .iter()
599 .map(|link| link.version_id)
600 .chain(value.outputs.iter().map(|link| link.version_id))
601 {
602 if visible.contains_key(&version_id.0) {
603 continue;
604 }
605 let version = self
606 .assets
607 .get_asset_version(version_id)
608 .await
609 .map_err(|error| core_error("resolve provenance version", error))?;
610 let allowed = match version {
611 Some(version) => self
612 .assets
613 .get_asset_by_id(version.asset_id)
614 .await
615 .map_err(|error| core_error("authorize provenance Asset", error))?
616 .is_some(),
617 None => false,
618 };
619 visible.insert(version_id.0, allowed);
620 }
621 }
622 for value in &mut values {
623 value
624 .inputs
625 .retain(|link| visible.get(&link.version_id.0) == Some(&true));
626 value
627 .outputs
628 .retain(|link| visible.get(&link.version_id.0) == Some(&true));
629 }
630 let datastore_id = self.catalogue_datastore_id()?;
631 Ok(values
632 .into_iter()
633 .map(|value| crate::projections::semantic_transformation(datastore_id, value))
634 .collect())
635 }
636
637 pub(super) async fn semantic_domains(
638 &self,
639 domain: Option<DomainSelector>,
640 study: Option<StudySelector>,
641 label: &str,
642 ) -> Result<Vec<DomainRecord>, AppError> {
643 match (domain, study) {
644 (Some(domain), study) => {
645 let domain = self.resolve_domain_selector(domain, label).await?;
646 if let Some(study) = study {
647 let study = self.resolve_study_selector(study, label).await?;
648 let domains = self
649 .study_domains
650 .list_domains_for_study(study.study_id)
651 .await
652 .map_err(|e| core_error("list semantic Study domains", e))?;
653 if !domains.iter().any(|d| d.domain_id == domain.domain_id) {
654 return Err(AppError::Conflict(
655 "Domain does not belong to the selected Study".into(),
656 ));
657 }
658 }
659 Ok(vec![domain])
660 }
661 (None, Some(study)) => {
662 let study = self.resolve_study_selector(study, label).await?;
663 self.study_domains
664 .list_domains_for_study(study.study_id)
665 .await
666 .map_err(|e| core_error("list semantic Study domains", e))
667 }
668 (None, None) => Err(AppError::Validation(
669 "Semantic listing requires a Domain or Study".into(),
670 )),
671 }
672 }
673}
674
675fn schema_item(item: ahri_tre_types::VocabularyItemRecord) -> dictionary::VocabularyItemSummary {
676 dictionary::VocabularyItemSummary {
677 value: item.value,
678 code: item.code,
679 description: item.description,
680 }
681}
682
683fn semantic_mutation_error(error: AppError) -> CoreError {
684 match error {
685 AppError::Validation(message) => CoreError::Validation(message),
686 AppError::NotFound(message) => CoreError::NotFound(message),
687 AppError::Conflict(message) => CoreError::Conflict(message),
688 AppError::NotImplemented(message) => CoreError::NotImplemented(message),
689 _ => CoreError::Infrastructure("Semantic mutation is unavailable".into()),
690 }
691}