diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/TermController.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/TermController.java index c8ee8d1d2..5d7e6fc9f 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/TermController.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/TermController.java @@ -6,6 +6,9 @@ import org.opendatadiscovery.oddplatform.api.contract.model.CountableSearchFilter; import org.opendatadiscovery.oddplatform.api.contract.model.DataEntityList; import org.opendatadiscovery.oddplatform.api.contract.model.DatasetFieldList; +import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTerm; +import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTermFormData; +import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTermList; import org.opendatadiscovery.oddplatform.api.contract.model.MultipleFacetType; import org.opendatadiscovery.oddplatform.api.contract.model.Ownership; import org.opendatadiscovery.oddplatform.api.contract.model.OwnershipFormData; @@ -111,6 +114,15 @@ public Mono> getTermLinkedColumns(final Long te .map(ResponseEntity::ok); } + @Override + public Mono> getTermLinkedTerms(final Long termId, final Integer page, + final Integer size, final String query, + final ServerWebExchange exchange) { + return termService + .listByTerm(termId, query, page, size) + .map(ResponseEntity::ok); + } + @Override public Mono>> createTermTagsRelations(final Long termId, final Mono tagsFormData, @@ -217,4 +229,20 @@ public Mono> deleteQueryExampleToTermRelationship(final Lon return queryExampleService.removeTermFromQueryExample(termId, exampleId) .thenReturn(ResponseEntity.noContent().build()); } + + @Override + public Mono> addLinkedTermToTerm(final Long termId, + final Mono linkedTermFormData, + final ServerWebExchange exchange) { + return linkedTermFormData + .flatMap(fd -> termService.linkTermWithTerm(fd.getLinkedTermId(), termId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono> deleteLinkedTermFromTerm(final Long termId, final Long linkedTermId, + final ServerWebExchange exchange) { + return termService.removeTermToLinkedTermRelation(termId, linkedTermId) + .thenReturn(ResponseEntity.noContent().build()); + } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDetailsDto.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDetailsDto.java index 515c7a2c4..ec1ba65f9 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDetailsDto.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDetailsDto.java @@ -1,5 +1,6 @@ package org.opendatadiscovery.oddplatform.dto.term; +import java.util.List; import java.util.Set; import lombok.Builder; import lombok.Getter; @@ -12,9 +13,11 @@ public class TermDetailsDto { private final TermDto termDto; private final Set tags; + private final List terms; public TermDetailsDto(final TermRefDto termRefDto) { this.tags = null; - this.termDto = new TermDto(termRefDto, null, null, null, null); + this.terms = null; + this.termDto = new TermDto(termRefDto, null, null, null, null, null); } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDto.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDto.java index 4c0253edc..7e7f27194 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDto.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/dto/term/TermDto.java @@ -10,6 +10,7 @@ public class TermDto { private final TermRefDto termRefDto; private final Integer entitiesUsingCount; private final Integer columnsUsingCount; + private final Integer linkedTermsUsingCount; private final Integer queryExampleUsingCount; private final Set ownerships; } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/mapper/TermMapper.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/mapper/TermMapper.java index 4d3c9faa7..aeb4125ba 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/mapper/TermMapper.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/mapper/TermMapper.java @@ -75,6 +75,7 @@ default TermList mapToPage(final Page page) { @Mapping(source = "dto.termDto.ownerships", target = "ownership") @Mapping(source = "dto.termDto.entitiesUsingCount", target = "entitiesUsingCount") @Mapping(source = "dto.termDto.columnsUsingCount", target = "columnsUsingCount") + @Mapping(source = "dto.termDto.linkedTermsUsingCount", target = "linkedTermsUsingCount") @Mapping(source = "dto.termDto.queryExampleUsingCount", target = "queryExampleUsingCount") TermDetails mapToDetails(final TermDetailsDto dto); diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepository.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepository.java index 8f72f0c2b..81b5d2f18 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepository.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepository.java @@ -37,4 +37,10 @@ public interface ReactiveTermRepository extends ReactiveCRUDRepository Flux getDatasetFieldTerms(final long datasetFieldId); Mono hasDescriptionRelations(final long termId); + + Flux getLinkedTermsByTargetTermId(long targetTermId); + + Flux listByTerm(final Long termId, final String query, final Integer page, final Integer size); + + Mono getTermByIdAndLinkedTermId(final Long assignedTermId, final Long targetTermId); } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepositoryImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepositoryImpl.java index 403892de4..023069a7b 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepositoryImpl.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/ReactiveTermRepositoryImpl.java @@ -32,8 +32,11 @@ import org.opendatadiscovery.oddplatform.model.tables.pojos.TagPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.TermOwnershipPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.TermPojo; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermToTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.TitlePojo; +import org.opendatadiscovery.oddplatform.model.tables.records.NamespaceRecord; import org.opendatadiscovery.oddplatform.model.tables.records.TermRecord; +import org.opendatadiscovery.oddplatform.model.tables.records.TermToTermRecord; import org.opendatadiscovery.oddplatform.repository.util.JooqFTSHelper; import org.opendatadiscovery.oddplatform.repository.util.JooqQueryHelper; import org.opendatadiscovery.oddplatform.repository.util.JooqReactiveOperations; @@ -63,6 +66,7 @@ import static org.opendatadiscovery.oddplatform.model.Tables.TERM; import static org.opendatadiscovery.oddplatform.model.Tables.TERM_OWNERSHIP; import static org.opendatadiscovery.oddplatform.model.Tables.TERM_SEARCH_ENTRYPOINT; +import static org.opendatadiscovery.oddplatform.model.Tables.TERM_TO_TERM; import static org.opendatadiscovery.oddplatform.model.Tables.TITLE; import static org.opendatadiscovery.oddplatform.repository.util.FTSConstants.RANK_FIELD_ALIAS; import static org.opendatadiscovery.oddplatform.repository.util.FTSConstants.TERM_CONDITIONS; @@ -76,8 +80,12 @@ public class ReactiveTermRepositoryImpl extends ReactiveAbstractSoftDeleteCRUDRe private static final String AGG_OWNERSHIPS_FIELD = "ownerships"; private static final String AGG_TITLES_FIELD = "titles"; private static final String AGG_TAGS_FIELD = "tags"; + private static final String AGG_ASSIGNED_TERMS = "assigned_terms"; + public static final String ASSIGNED_TERM_NAMESPACES = "assigned_term_namespaces"; + public static final String ASSIGNED_TERM_RELATIONS = "assigned_term_relation"; private static final String ENTITIES_COUNT = "entities_count"; private static final String COLUMNS_COUNT = "columns_count"; + private static final String LINKED_TERMS_COUNT = "linked_terms_count"; private static final String QUERY_EXAMPLE_COUNT = "query_example_count"; private static final String IS_DESCRIPTION_LINK = "is_description_link"; @@ -177,6 +185,11 @@ public Mono getTermRefDto(final Long id) { @Override public Mono getTermDetailsDto(final Long id) { + final Table assignedTerms = TERM.asTable("assigned_terms"); + final Table assignedTermsNamespace = NAMESPACE.asTable("assigned_terms_namespace"); + final Table assignedTermRelations = TERM_TO_TERM.asTable("assigned_term_relations"); + final Table linkedTerms = TERM_TO_TERM.asTable("linked_terms"); + final List> groupByFields = Stream.of(TERM.fields(), NAMESPACE.fields()) .flatMap(Arrays::stream) .toList(); @@ -187,9 +200,13 @@ public Mono getTermDetailsDto(final Long id) { .select(jsonArrayAgg(field(OWNER.asterisk().toString())).as(AGG_OWNERS_FIELD)) .select(jsonArrayAgg(field(TITLE.asterisk().toString())).as(AGG_TITLES_FIELD)) .select(jsonArrayAgg(field(TAG.asterisk().toString())).as(AGG_TAGS_FIELD)) + .select(jsonArrayAgg(field(assignedTerms.asterisk().toString())).as(AGG_ASSIGNED_TERMS)) + .select(jsonArrayAgg(field(NAMESPACE.asterisk().toString())).as(ASSIGNED_TERM_NAMESPACES)) + .select(jsonArrayAgg(field(assignedTermRelations.asterisk().toString())).as(ASSIGNED_TERM_RELATIONS)) .select(DSL.countDistinct(DATA_ENTITY_TO_TERM.DATA_ENTITY_ID).as(ENTITIES_COUNT)) .select(DSL.countDistinct(DATASET_FIELD_TO_TERM.DATASET_FIELD_ID).as(COLUMNS_COUNT)) .select(DSL.countDistinct(QUERY_EXAMPLE_TO_TERM.QUERY_EXAMPLE_ID).as(QUERY_EXAMPLE_COUNT)) + .select(DSL.countDistinct(linkedTerms.field(TERM_TO_TERM.TARGET_TERM_ID)).as(LINKED_TERMS_COUNT)) .from(TERM) .join(NAMESPACE).on(NAMESPACE.ID.eq(TERM.NAMESPACE_ID)) .leftJoin(TERM_OWNERSHIP).on(TERM_OWNERSHIP.TERM_ID.eq(TERM.ID)) @@ -200,6 +217,13 @@ public Mono getTermDetailsDto(final Long id) { .leftJoin(DATA_ENTITY_TO_TERM).on(DATA_ENTITY_TO_TERM.TERM_ID.eq(TERM.ID)) .leftJoin(DATASET_FIELD_TO_TERM).on(DATASET_FIELD_TO_TERM.TERM_ID.eq(TERM.ID)) .leftJoin(QUERY_EXAMPLE_TO_TERM).on(QUERY_EXAMPLE_TO_TERM.TERM_ID.eq(TERM.ID)) + .leftJoin(linkedTerms).on(linkedTerms.field(TERM_TO_TERM.ASSIGNED_TERM_ID).eq(TERM.ID)) + .leftJoin(assignedTermRelations) + .on(assignedTermRelations.field(TERM_TO_TERM.TARGET_TERM_ID).eq(TERM.ID)) + .leftJoin(assignedTerms) + .on(assignedTerms.field(TERM.ID).eq(assignedTermRelations.field(TERM_TO_TERM.ASSIGNED_TERM_ID))) + .leftJoin(assignedTermsNamespace) + .on(assignedTerms.field(TERM.NAMESPACE_ID).eq(assignedTermsNamespace.field(NAMESPACE.ID))) .where(TERM.ID.eq(id).and(TERM.DELETED_AT.isNull())) .groupBy(groupByFields); return jooqReactiveOperations.mono(query) @@ -290,6 +314,8 @@ public Mono> findByState(final FacetStateDto state, final int page .flatMap(Arrays::stream) .toList(); + final Table linkedTerms = TERM_TO_TERM.asTable("linked_terms"); + final var query = DSL.with(termCTE.getName()) .as(termSelect) .select(termCTE.fields()) @@ -300,6 +326,7 @@ public Mono> findByState(final FacetStateDto state, final int page .select(DSL.countDistinct(DATA_ENTITY_TO_TERM.DATA_ENTITY_ID).as(ENTITIES_COUNT)) .select(DSL.countDistinct(DATASET_FIELD_TO_TERM.DATASET_FIELD_ID).as(COLUMNS_COUNT)) .select(DSL.countDistinct(QUERY_EXAMPLE_TO_TERM.QUERY_EXAMPLE_ID).as(QUERY_EXAMPLE_COUNT)) + .select(DSL.countDistinct(linkedTerms.field(TERM_TO_TERM.TARGET_TERM_ID)).as(LINKED_TERMS_COUNT)) .from(termCTE.getName()) .join(NAMESPACE).on(NAMESPACE.ID.eq(termCTE.field(TERM.NAMESPACE_ID))) .leftJoin(TERM_OWNERSHIP).on(TERM_OWNERSHIP.TERM_ID.eq(termCTE.field(TERM.ID))) @@ -308,6 +335,7 @@ public Mono> findByState(final FacetStateDto state, final int page .leftJoin(DATA_ENTITY_TO_TERM).on(DATA_ENTITY_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) .leftJoin(DATASET_FIELD_TO_TERM).on(DATASET_FIELD_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) .leftJoin(QUERY_EXAMPLE_TO_TERM).on(QUERY_EXAMPLE_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) + .leftJoin(linkedTerms).on(linkedTerms.field(TERM_TO_TERM.ASSIGNED_TERM_ID).eq(termCTE.field(TERM.ID))) .groupBy(groupByFields); return jooqReactiveOperations.flux(query) @@ -389,10 +417,108 @@ public Mono hasDescriptionRelations(final long termId) { .and(DATASET_FIELD_TO_TERM.IS_DESCRIPTION_LINK.isTrue()) .and(DATA_ENTITY.STATUS.ne(DataEntityStatusDto.DELETED.getId())) )); - final var query = DSL.select(dataEntityDescriptionRelations.or(datasetFieldDescriptionRelations)); + final Condition termDescriptionRelations = exists(DSL.selectOne() + .from(TERM_TO_TERM) + .join(TERM).on(TERM_TO_TERM.TARGET_TERM_ID.eq(TERM.ID)) + .where(TERM_TO_TERM.ASSIGNED_TERM_ID.eq(termId) + .and(TERM_TO_TERM.IS_DESCRIPTION_LINK.isTrue()) + .and(TERM.DELETED_AT.isNull()) + )); + final var query = DSL.select(dataEntityDescriptionRelations + .or(datasetFieldDescriptionRelations) + .or(termDescriptionRelations)); return jooqReactiveOperations.mono(query).map(Record1::component1); } + @Override + public Flux getLinkedTermsByTargetTermId(final long targetTermId) { + final var query = DSL + .select(TERM.fields()) + .select(NAMESPACE.fields()) + .select(TERM_TO_TERM.IS_DESCRIPTION_LINK.as(IS_DESCRIPTION_LINK)) + .from(TERM) + .join(NAMESPACE).on(NAMESPACE.ID.eq(TERM.NAMESPACE_ID)) + .join(TERM_TO_TERM) + .on(TERM_TO_TERM.ASSIGNED_TERM_ID.eq(TERM.ID) + .and(TERM_TO_TERM.TARGET_TERM_ID.eq(targetTermId))) + .where(TERM.DELETED_AT.isNull()); + return jooqReactiveOperations.flux(query) + .map(this::mapRecordToLinkedTermDto); + } + + @Override + public Flux listByTerm(final Long termId, final String query, + final Integer page, final Integer size) { + final List conditions = new ArrayList<>(); + + conditions.add(TERM.DELETED_AT.isNull()); + + if (StringUtils.isNotBlank(query)) { + conditions.add(TERM.NAME.containsIgnoreCase(query)); + } + + final var baseQuery = DSL.select(TERM.fields()) + .from(TERM) + .where(conditions) + .orderBy(TERM.ID.desc()); + + final Table termCTE = baseQuery.asTable("term_cte"); + final Table assignedTermRelations = TERM_TO_TERM.asTable("assigned_term_relations"); + + final List> groupByFields = Stream.of(termCTE.fields(), NAMESPACE.fields(), + assignedTermRelations.fields(TERM_TO_TERM.IS_DESCRIPTION_LINK)) + .flatMap(Arrays::stream) + .toList(); + + final var finalQuery = DSL.with(termCTE.getName()) + .as(baseQuery) + .select(termCTE.fields()) + .select(NAMESPACE.fields()) + .select(assignedTermRelations.field(TERM_TO_TERM.IS_DESCRIPTION_LINK).as(IS_DESCRIPTION_LINK)) + .from(termCTE) + .join(NAMESPACE).on(NAMESPACE.ID.eq(termCTE.field(TERM.NAMESPACE_ID))) + .leftJoin(DATA_ENTITY_TO_TERM).on(DATA_ENTITY_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) + .leftJoin(DATASET_FIELD_TO_TERM).on(DATASET_FIELD_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) + .leftJoin(assignedTermRelations) + .on(assignedTermRelations.field(TERM_TO_TERM.TARGET_TERM_ID).eq(termCTE.field(TERM.ID))) + .where(assignedTermRelations.field(TERM_TO_TERM.ASSIGNED_TERM_ID).eq(termId)) + .groupBy(groupByFields) + .orderBy(List.of(jooqQueryHelper.getField(termCTE, TERM.ID).desc())) + .limit(size) + .offset((page - 1) * size); + + return jooqReactiveOperations.flux(finalQuery) + .map(this::mapRecordToLinkedTermDto); + } + + @Override + public Mono getTermByIdAndLinkedTermId(final Long assignedTermId, final Long targetTermId) { + final var baseQuery = DSL.select(TERM.fields()) + .from(TERM) + .where(TERM.DELETED_AT.isNull()) + .and(TERM.ID.eq(targetTermId)) + .orderBy(TERM.ID.desc()); + + final Table termCTE = baseQuery.asTable("term_cte"); + final Table assignedTermRelations = TERM_TO_TERM.asTable("assigned_term_relations"); + + final var finalQuery = DSL.with(termCTE.getName()) + .as(baseQuery) + .select(termCTE.fields()) + .select(NAMESPACE.fields()) + .select(assignedTermRelations.field(TERM_TO_TERM.IS_DESCRIPTION_LINK).as(IS_DESCRIPTION_LINK)) + .from(termCTE) + .join(NAMESPACE).on(NAMESPACE.ID.eq(termCTE.field(TERM.NAMESPACE_ID))) + .leftJoin(DATA_ENTITY_TO_TERM).on(DATA_ENTITY_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) + .leftJoin(DATASET_FIELD_TO_TERM).on(DATASET_FIELD_TO_TERM.TERM_ID.eq(termCTE.field(TERM.ID))) + .leftJoin(assignedTermRelations) + .on(assignedTermRelations.field(TERM_TO_TERM.TARGET_TERM_ID).eq(termCTE.field(TERM.ID))) + .where(assignedTermRelations.field(TERM_TO_TERM.ASSIGNED_TERM_ID).eq(assignedTermId)); + + return jooqReactiveOperations.mono(finalQuery) + .map(this::mapRecordToLinkedTermDto); + } + private LinkedTermDto mapRecordToLinkedTermDto(final Record record) { final TermRefDto termRefDto = mapRecordToRefDto(record); return new LinkedTermDto(termRefDto, record.get(IS_DESCRIPTION_LINK, Boolean.class)); @@ -418,6 +544,7 @@ private TermDto mapRecordToDto(final Record record) { .termRefDto(refDto) .entitiesUsingCount(record.get(ENTITIES_COUNT, Integer.class)) .columnsUsingCount(record.get(COLUMNS_COUNT, Integer.class)) + .linkedTermsUsingCount(record.get(LINKED_TERMS_COUNT, Integer.class)) .queryExampleUsingCount(record.get(QUERY_EXAMPLE_COUNT, Integer.class)) .ownerships(extractOwnershipRelation(record)) .build(); @@ -429,6 +556,7 @@ private TermDto mapRecordToDto(final Record record, final String cteName) { .termRefDto(refDto) .entitiesUsingCount(record.get(ENTITIES_COUNT, Integer.class)) .columnsUsingCount(record.get(COLUMNS_COUNT, Integer.class)) + .linkedTermsUsingCount(record.get(LINKED_TERMS_COUNT, Integer.class)) .queryExampleUsingCount(record.get(QUERY_EXAMPLE_COUNT, Integer.class)) .ownerships(extractOwnershipRelation(record)) .build(); @@ -439,6 +567,7 @@ private TermDetailsDto mapRecordToDetailsDto(final Record record) { return TermDetailsDto.builder() .termDto(termDto) .tags(jooqRecordHelper.extractAggRelation(record, AGG_TAGS_FIELD, TagPojo.class)) + .terms(extractTerms(record)) .build(); } @@ -470,4 +599,32 @@ private Set extractOwnershipRelation(final Record r) { }) .collect(Collectors.toSet()); } + + private List extractTerms(final Record record) { + final Set terms = + jooqRecordHelper.extractAggRelation(record, AGG_ASSIGNED_TERMS, TermPojo.class); + + final Map namespaces = jooqRecordHelper + .extractAggRelation(record, ASSIGNED_TERM_NAMESPACES, NamespacePojo.class) + .stream() + .collect(Collectors.toMap(NamespacePojo::getId, identity())); + + final Map> relations = jooqRecordHelper + .extractAggRelation(record, ASSIGNED_TERM_RELATIONS, TermToTermPojo.class) + .stream() + .collect(Collectors.groupingBy(TermToTermPojo::getAssignedTermId)); + + return terms.stream() + .map(pojo -> { + final TermRefDto termRefDto = TermRefDto.builder() + .term(pojo) + .namespace(namespaces.get(pojo.getNamespaceId())) + .build(); + final boolean isDescriptionLink = relations.getOrDefault(pojo.getId(), List.of()).stream() + .anyMatch(r -> Boolean.TRUE.equals(r.getIsDescriptionLink())); + + return new LinkedTermDto(termRefDto, isDescriptionLink); + }) + .toList(); + } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepository.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepository.java new file mode 100644 index 000000000..440151894 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepository.java @@ -0,0 +1,15 @@ +package org.opendatadiscovery.oddplatform.repository.reactive; + +import java.util.List; +import org.opendatadiscovery.oddplatform.dto.term.TermBaseInfoDto; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermDefinitionUnhandledTermPojo; +import reactor.core.publisher.Flux; + +public interface TermDefinitionUnhandledTermRepository { + + Flux deleteForTermExceptSpecified(long termId, + final List termsToKeep); + + Flux createUnhandledTerms( + final List unhandledTerms); +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepositoryImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepositoryImpl.java new file mode 100644 index 000000000..594fca928 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermDefinitionUnhandledTermRepositoryImpl.java @@ -0,0 +1,71 @@ +package org.opendatadiscovery.oddplatform.repository.reactive; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.jooq.Condition; +import org.jooq.Row2; +import org.jooq.impl.DSL; +import org.opendatadiscovery.oddplatform.dto.term.TermBaseInfoDto; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermDefinitionUnhandledTermPojo; +import org.opendatadiscovery.oddplatform.model.tables.records.TermDefinitionUnhandledTermRecord; +import org.opendatadiscovery.oddplatform.repository.util.JooqReactiveOperations; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +import static org.jooq.impl.DSL.lower; +import static org.jooq.impl.DSL.row; +import static org.opendatadiscovery.oddplatform.model.Keys.TERM_DEFINITION_UNHANDLED_TERM_UNIQUE_KEY; +import static org.opendatadiscovery.oddplatform.model.Tables.TERM_DEFINITION_UNHANDLED_TERM; + +@Repository +@RequiredArgsConstructor +public class TermDefinitionUnhandledTermRepositoryImpl implements TermDefinitionUnhandledTermRepository { + + private final JooqReactiveOperations jooqReactiveOperations; + + @Override + public Flux + deleteForTermExceptSpecified(final long termId, + final List termsToKeep) { + final List> termRows = termsToKeep.stream() + .map(term -> row(lower(term.name()), lower(term.namespaceName()))) + .toList(); + final Condition condition; + if (CollectionUtils.isNotEmpty(termRows)) { + condition = row(lower(TERM_DEFINITION_UNHANDLED_TERM.TERM_NAME), + lower(TERM_DEFINITION_UNHANDLED_TERM.TERM_NAMESPACE_NAME)).notIn(termRows); + } else { + condition = DSL.noCondition(); + } + final var query = DSL.deleteFrom(TERM_DEFINITION_UNHANDLED_TERM) + .where(TERM_DEFINITION_UNHANDLED_TERM.TARGET_TERM_ID.eq(termId)).and(condition) + .returning(); + return jooqReactiveOperations.flux(query) + .map(r -> r.into(TermDefinitionUnhandledTermPojo.class)); + } + + @Override + public Flux createUnhandledTerms( + final List unhandledTerms) { + if (unhandledTerms.isEmpty()) { + return Flux.just(); + } + final List records = unhandledTerms.stream() + .map(p -> jooqReactiveOperations.newRecord(TERM_DEFINITION_UNHANDLED_TERM, p)) + .toList(); + + var insertStep = DSL.insertInto(TERM_DEFINITION_UNHANDLED_TERM); + + for (int i = 0; i < records.size() - 1; i++) { + insertStep = insertStep.set(records.get(i)).newRecord(); + } + + return jooqReactiveOperations.flux( + insertStep.set(records.get(records.size() - 1)) + .onConflictOnConstraint(TERM_DEFINITION_UNHANDLED_TERM_UNIQUE_KEY) + .doNothing() + .returning(TERM_DEFINITION_UNHANDLED_TERM.fields()) + ).map(r -> r.into(TermDefinitionUnhandledTermPojo.class)); + } +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepository.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepository.java index fbb6bafb9..da9299292 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepository.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepository.java @@ -3,6 +3,7 @@ import java.util.List; import org.opendatadiscovery.oddplatform.model.tables.pojos.DataEntityToTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.DatasetFieldToTermPojo; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermToTermPojo; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -23,7 +24,15 @@ public interface TermRelationsRepository { Flux createRelationsWithDatasetField(final List relations); + Flux createRelationsWithTerm(final List relations); + + Mono createRelationWithTerm(final Long linkedTermId, final Long termId); + Mono deleteRelationWithDatasetField(final long datasetFieldId, final long termId); Flux deleteTermDatasetFieldRelations(final List pojos); + + Flux deleteTermToTermRelations(final List pojos); + + Mono deleteTermToLinkedTermRelation(final Long linkedTermId, final Long termId); } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepositoryImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepositoryImpl.java index 760667aff..1c5341100 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepositoryImpl.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/repository/reactive/TermRelationsRepositoryImpl.java @@ -7,8 +7,10 @@ import org.jooq.impl.DSL; import org.opendatadiscovery.oddplatform.model.tables.pojos.DataEntityToTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.DatasetFieldToTermPojo; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermToTermPojo; import org.opendatadiscovery.oddplatform.model.tables.records.DataEntityToTermRecord; import org.opendatadiscovery.oddplatform.model.tables.records.DatasetFieldToTermRecord; +import org.opendatadiscovery.oddplatform.model.tables.records.TermToTermRecord; import org.opendatadiscovery.oddplatform.repository.util.JooqReactiveOperations; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -16,6 +18,7 @@ import static org.opendatadiscovery.oddplatform.model.Tables.DATASET_FIELD_TO_TERM; import static org.opendatadiscovery.oddplatform.model.Tables.DATA_ENTITY_TO_TERM; +import static org.opendatadiscovery.oddplatform.model.Tables.TERM_TO_TERM; @RequiredArgsConstructor @Repository @@ -135,6 +138,39 @@ public Flux createRelationsWithDatasetField(final List r.into(DatasetFieldToTermPojo.class)); } + @Override + public Flux createRelationsWithTerm(final List relations) { + if (relations.isEmpty()) { + return Flux.just(); + } + final List records = relations.stream() + .map(p -> jooqReactiveOperations.newRecord(TERM_TO_TERM, p)) + .toList(); + + var insertStep = DSL.insertInto(TERM_TO_TERM); + + for (int i = 0; i < records.size() - 1; i++) { + insertStep = insertStep.set(records.get(i)).newRecord(); + } + + return jooqReactiveOperations.flux( + insertStep.set(records.get(records.size() - 1)) + .onDuplicateKeyIgnore() + .returning(TERM_TO_TERM.fields()) + ).map(r -> r.into(TermToTermPojo.class)); + } + + @Override + public Mono createRelationWithTerm(final Long linkedTermId, final Long termId) { + final var query = DSL.insertInto(TERM_TO_TERM) + .set(TERM_TO_TERM.TARGET_TERM_ID, termId) + .set(TERM_TO_TERM.ASSIGNED_TERM_ID, linkedTermId) + .onDuplicateKeyIgnore() + .returning(); + return jooqReactiveOperations.mono(query) + .map(r -> r.into(TermToTermPojo.class)); + } + @Override public Mono deleteRelationWithDatasetField(final long datasetFieldId, final long termId) { final var query = DSL.deleteFrom(DATASET_FIELD_TO_TERM) @@ -163,4 +199,33 @@ public Flux deleteTermDatasetFieldRelations(final List r.into(DatasetFieldToTermPojo.class)); } + + @Override + public Flux deleteTermToTermRelations(final List pojos) { + if (CollectionUtils.isEmpty(pojos)) { + return Flux.just(); + } + final Condition condition = pojos.stream() + .map(pojo -> TERM_TO_TERM.TARGET_TERM_ID.eq(pojo.getTargetTermId()) + .and(TERM_TO_TERM.ASSIGNED_TERM_ID.eq(pojo.getAssignedTermId())) + .and(TERM_TO_TERM.IS_DESCRIPTION_LINK.eq(pojo.getIsDescriptionLink()))) + .reduce(Condition::or) + .orElseThrow(() -> new RuntimeException("Couldn't build condition for deletion")); + final var query = DSL.delete(TERM_TO_TERM) + .where(condition) + .returning(); + return jooqReactiveOperations.flux(query) + .map(r -> r.into(TermToTermPojo.class)); + } + + @Override + public Mono deleteTermToLinkedTermRelation(final Long linkedTermId, final Long termId) { + final var query = DSL.deleteFrom(TERM_TO_TERM) + .where(TERM_TO_TERM.ASSIGNED_TERM_ID.eq(linkedTermId) + .and(TERM_TO_TERM.TARGET_TERM_ID.eq(termId)) + .and(TERM_TO_TERM.IS_DESCRIPTION_LINK.isFalse())) + .returning(); + return jooqReactiveOperations.mono(query) + .map(r -> r.into(TermToTermPojo.class)); + } } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermService.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermService.java index a36910aa6..b44243caa 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermService.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermService.java @@ -2,6 +2,7 @@ import java.util.List; import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTerm; +import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTermList; import org.opendatadiscovery.oddplatform.api.contract.model.Tag; import org.opendatadiscovery.oddplatform.api.contract.model.TagsFormData; import org.opendatadiscovery.oddplatform.api.contract.model.TermDetails; @@ -44,4 +45,10 @@ Mono> handleDatasetFieldDescriptionTerms(final long datasetF Mono> getDataEntityTerms(final long dataEntityId); Mono> getDatasetFieldTerms(final long datasetFieldId); + + Mono listByTerm(final Long termId, final String query, final Integer page, final Integer size); + + Mono linkTermWithTerm(final Long linkedTermId, final Long termId); + + Mono removeTermToLinkedTermRelation(final Long termId, final Long linkedTermId); } diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermServiceImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermServiceImpl.java index da9c67ccc..c89d188fa 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermServiceImpl.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/term/TermServiceImpl.java @@ -12,6 +12,8 @@ import org.apache.commons.lang3.StringUtils; import org.opendatadiscovery.oddplatform.annotation.ReactiveTransactional; import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTerm; +import org.opendatadiscovery.oddplatform.api.contract.model.LinkedTermList; +import org.opendatadiscovery.oddplatform.api.contract.model.PageInfo; import org.opendatadiscovery.oddplatform.api.contract.model.Tag; import org.opendatadiscovery.oddplatform.api.contract.model.TagsFormData; import org.opendatadiscovery.oddplatform.api.contract.model.TermDetails; @@ -33,11 +35,14 @@ import org.opendatadiscovery.oddplatform.model.tables.pojos.DatasetFieldDescriptionUnhandledTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.DatasetFieldToTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.NamespacePojo; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermDefinitionUnhandledTermPojo; import org.opendatadiscovery.oddplatform.model.tables.pojos.TermPojo; +import org.opendatadiscovery.oddplatform.model.tables.pojos.TermToTermPojo; import org.opendatadiscovery.oddplatform.repository.reactive.DataEntityDescriptionUnhandledTermRepository; import org.opendatadiscovery.oddplatform.repository.reactive.DatasetFieldDescriptionUnhandledTermRepositoryImpl; import org.opendatadiscovery.oddplatform.repository.reactive.ReactiveTermRepository; import org.opendatadiscovery.oddplatform.repository.reactive.ReactiveTermSearchEntrypointRepository; +import org.opendatadiscovery.oddplatform.repository.reactive.TermDefinitionUnhandledTermRepository; import org.opendatadiscovery.oddplatform.repository.reactive.TermRelationsRepository; import org.opendatadiscovery.oddplatform.service.DataEntityFilledService; import org.opendatadiscovery.oddplatform.service.NamespaceService; @@ -66,6 +71,7 @@ public class TermServiceImpl implements TermService { private final ReactiveTermRepository termRepository; private final TermRelationsRepository termRelationsRepository; + private final TermDefinitionUnhandledTermRepository termDefinitionUnhandledTermRepository; private final ReactiveTermSearchEntrypointRepository termSearchEntrypointRepository; private final DataEntityDescriptionUnhandledTermRepository dataEntityDescriptionUnhandledTermRepository; private final DatasetFieldDescriptionUnhandledTermRepositoryImpl datasetFieldDescriptionUnhandledTermRepository; @@ -92,7 +98,8 @@ public Mono getTermByNamespaceAndName(final String namespaceName, final public Mono createTerm(final TermFormData formData) { final Mono createTermMono = Mono.defer(() -> namespaceService .getOrCreate(formData.getNamespaceName()) - .flatMap(namespace -> create(formData, namespace))); + .zipWith(findTermsInDescription(formData.getDefinition())) + .flatMap(tuple -> create(formData, tuple.getT1(), tuple.getT2()))); return termRepository.getByNameAndNamespace(formData.getNamespaceName(), formData.getName()) .handle((dto, sink) -> { @@ -265,18 +272,49 @@ public Mono> getDatasetFieldTerms(final long datasetFieldId) return removeDuplicateNonDescriptionTerms(datasetFieldTerms).collectList(); } + @Override + public Mono listByTerm(final Long termId, final String query, + final Integer page, final Integer size) { + return termRepository.listByTerm(termId, query, page, size) + .collectList() + .map(item -> new LinkedTermList() + .items(termMapper.mapListToLinkedTermList(item)) + .pageInfo(new PageInfo().total((long) item.size()).hasNext(false))); + } + + @Override + @ReactiveTransactional + public Mono linkTermWithTerm(final Long linkedTermId, final Long termId) { + return termRelationsRepository.createRelationWithTerm(linkedTermId, termId) + .flatMap(relation -> termRepository.getTermRefDto(relation.getAssignedTermId())) + .map(termRefDto -> new LinkedTermDto(termRefDto, false)) + .map(termMapper::mapToLinkedTerm); + } + + @Override + @ReactiveTransactional + public Mono removeTermToLinkedTermRelation(final Long termId, final Long linkedTermId) { + return termRelationsRepository.deleteTermToLinkedTermRelation(linkedTermId, termId) + .then(); + } + private Mono update(final TermPojo pojo) { return termRepository.update(pojo) + .flatMap(term -> findTermsInDescription(term.getDefinition()) + .flatMap(linkedTerms -> updateTermDefinitionTermsState(linkedTerms, term.getId())) + .thenReturn(term)) .flatMap(term -> termRepository.getTermDetailsDto(term.getId())) .map(termMapper::mapToDetails); } private Mono create(final TermFormData formData, - final NamespacePojo namespace) { + final NamespacePojo namespace, + final DescriptionParsedTerms linkedTerms) { final TermPojo pojo = termMapper.mapToPojo(formData, namespace); return termRepository .create(pojo) - .map(term -> TermRefDto.builder().term(term).namespace(namespace).build()) + .flatMap(term -> updateTermDefinitionTermsState(linkedTerms, term.getId()) + .thenReturn(TermRefDto.builder().term(term).namespace(namespace).build())) .map(termRefDto -> termMapper.mapToDetails(new TermDetailsDto(termRefDto))); } @@ -406,6 +444,37 @@ private Flux removeDuplicateNonDescriptionTerms(final Flux group.reduce((dto1, dto2) -> dto1.isDescriptionLink() ? dto1 : dto2)); } + private Mono updateTermDefinitionTermsState(final DescriptionParsedTerms terms, + final long termId) { + final Mono> existingDescriptionRelations = termRepository + .getLinkedTermsByTargetTermId(termId) + .filter(LinkedTermDto::isDescriptionLink) + .collectList(); + + return existingDescriptionRelations.flatMap(existing -> { + final List relationsToDelete = existing.stream() + .map(dto -> dto.term().getTerm()) + .filter(pojo -> !terms.foundTerms().contains(pojo)) + .map(pojo -> new TermToTermPojo() + .setTargetTermId(termId) + .setAssignedTermId(pojo.getId()) + .setIsDescriptionLink(true)) + .toList(); + + final List relations = + buildTermDescriptionTermRelations(terms.foundTerms(), termId); + final List unknownPojos = + buildTermUnknownTerms(terms.unknownTerms(), termId); + + return termRelationsRepository.deleteTermToTermRelations(relationsToDelete) + .thenMany(termRelationsRepository.createRelationsWithTerm(relations)) + .thenMany(termDefinitionUnhandledTermRepository + .deleteForTermExceptSpecified(termId, terms.unknownTerms())) + .thenMany(termDefinitionUnhandledTermRepository.createUnhandledTerms(unknownPojos)) + .then(); + }); + } + private List buildDataEntityDescriptionTermRelations(final List terms, final long dataEntityId) { return terms.stream() @@ -426,6 +495,18 @@ private List buildDatasetFieldDescriptionTermRelations(f .toList(); } + private List buildDatasetFieldUnknownTerms( + final List terms, + final long datasetFieldId) { + return terms.stream() + .map(t -> new DatasetFieldDescriptionUnhandledTermPojo() + .setDatasetFieldId(datasetFieldId) + .setTermName(t.name().toLowerCase()) + .setTermNamespaceName(t.namespaceName().toLowerCase()) + .setCreatedAt(DateTimeUtil.generateNow())) + .toList(); + } + private List buildDataEntityUnknownTerms(final List terms, final long dataEntityId) { return terms.stream() @@ -437,12 +518,22 @@ private List buildDataEntityUnknownTerms .toList(); } - private List buildDatasetFieldUnknownTerms( + private List buildTermDescriptionTermRelations(final List terms, + final long termId) { + return terms.stream() + .map(t -> new TermToTermPojo() + .setTargetTermId(termId) + .setAssignedTermId(t.getId()) + .setIsDescriptionLink(true)) + .toList(); + } + + private List buildTermUnknownTerms( final List terms, final long datasetFieldId) { return terms.stream() - .map(t -> new DatasetFieldDescriptionUnhandledTermPojo() - .setDatasetFieldId(datasetFieldId) + .map(t -> new TermDefinitionUnhandledTermPojo() + .setTargetTermId(datasetFieldId) .setTermName(t.name().toLowerCase()) .setTermNamespaceName(t.namespaceName().toLowerCase()) .setCreatedAt(DateTimeUtil.generateNow())) diff --git a/odd-platform-api/src/main/resources/db/migration/V0_0_91__add_term_to_term.sql b/odd-platform-api/src/main/resources/db/migration/V0_0_91__add_term_to_term.sql new file mode 100644 index 000000000..7edbf0b8a --- /dev/null +++ b/odd-platform-api/src/main/resources/db/migration/V0_0_91__add_term_to_term.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS term_to_term +( + target_term_id bigint NOT NULL, + assigned_term_id bigint NOT NULL, + is_description_link boolean DEFAULT FALSE, + deleted_at TIMESTAMP WITHOUT TIME ZONE, + + CONSTRAINT term_to_term_pk PRIMARY KEY (target_term_id, assigned_term_id), + + CONSTRAINT term_to_term_target_term_id_fkey FOREIGN KEY (target_term_id) REFERENCES term (id), + CONSTRAINT term_to_term_assigned_term_id_fkey FOREIGN KEY (assigned_term_id) REFERENCES term (id) +); + +CREATE TABLE IF NOT EXISTS term_definition_unhandled_term +( + id BIGSERIAL PRIMARY KEY, + target_term_id BIGINT NOT NULL, + term_name varchar(255) NOT NULL, + term_namespace_name varchar(64) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (NOW() AT TIME ZONE ('UTC')), + + CONSTRAINT fk_term_definition_unhandled_term_target_term_id FOREIGN KEY (target_term_id) REFERENCES term (id), + CONSTRAINT term_definition_unhandled_term_unique_key UNIQUE (target_term_id, term_name, term_namespace_name) +); \ No newline at end of file diff --git a/odd-platform-specification/components.yaml b/odd-platform-specification/components.yaml index 24025cb6e..decd7940b 100644 --- a/odd-platform-specification/components.yaml +++ b/odd-platform-specification/components.yaml @@ -2505,6 +2505,11 @@ components: type: array items: $ref: '#/components/schemas/LinkedTerm' + page_info: + $ref: '#/components/schemas/PageInfo' + required: + - items + - page_info LinkedTerm: type: object @@ -2572,6 +2577,8 @@ components: type: integer columns_using_count: type: integer + linked_terms_using_count: + type: integer query_example_using_count: type: integer created_at: @@ -2591,6 +2598,10 @@ components: properties: tags: $ref: '#/components/schemas/TagList' + terms: + type: array + items: + $ref: '#/components/schemas/LinkedTerm' TermFormData: type: object @@ -2633,6 +2644,15 @@ components: required: - query_example_id + LinkedTermFormData: + type: object + properties: + linked_term_id: + type: integer + format: int64 + required: + - linked_term_id + TermSearchFormData: type: object properties: @@ -4296,6 +4316,14 @@ components: type: integer format: int64 + LinkedTermIdParam: + name: linked_term_id + in: path + required: true + schema: + type: integer + format: int64 + PolicyIdParam: name: policy_id in: path diff --git a/odd-platform-specification/openapi.yaml b/odd-platform-specification/openapi.yaml index e8cd7074a..60e90c010 100644 --- a/odd-platform-specification/openapi.yaml +++ b/odd-platform-specification/openapi.yaml @@ -2873,6 +2873,26 @@ paths: tags: - term + /api/terms/{term_id}/linked_terms: + get: + summary: Get term linked terms + description: Get data columns, which are using this term + operationId: getTermLinkedTerms + parameters: + - $ref: './components.yaml/#/components/parameters/TermIdParam' + - $ref: './components.yaml/#/components/parameters/PageParam' + - $ref: './components.yaml/#/components/parameters/SizeParam' + - $ref: './components.yaml/#/components/parameters/SearchParam' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './components.yaml/#/components/schemas/LinkedTermList' + tags: + - term + /api/terms/{term_id}/queryexample: post: summary: add Term to QueryExample @@ -2910,6 +2930,43 @@ paths: tags: - term + /api/terms/{term_id}/term: + post: + summary: Add term to term + description: Link existing term with term + operationId: addLinkedTermToTerm + parameters: + - $ref: './components.yaml/#/components/parameters/TermIdParam' + requestBody: + required: true + content: + application/json: + schema: + $ref: './components.yaml/#/components/schemas/LinkedTermFormData' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './components.yaml/#/components/schemas/LinkedTerm' + tags: + - term + + /api/terms/{term_id}/term/{linked_term_id}: + delete: + summary: Delete term from term + description: Delete term from current linked terms list + operationId: deleteLinkedTermFromTerm + parameters: + - $ref: './components.yaml/#/components/parameters/TermIdParam' + - $ref: './components.yaml/#/components/parameters/LinkedTermIdParam' + responses: + '204': + $ref: './components.yaml/#/components/responses/Deleted' + tags: + - term + /api/terms/search: post: summary: Terms search by query diff --git a/odd-platform-ui/src/components/Terms/TermDetails/Overview/Overview.tsx b/odd-platform-ui/src/components/Terms/TermDetails/Overview/Overview.tsx index 736fd9592..92a059a6c 100644 --- a/odd-platform-ui/src/components/Terms/TermDetails/Overview/Overview.tsx +++ b/odd-platform-ui/src/components/Terms/TermDetails/Overview/Overview.tsx @@ -1,30 +1,32 @@ import { Grid, Typography } from '@mui/material'; -import React, { type FC } from 'react'; +import React, { useMemo, type FC } from 'react'; import { Markdown, SkeletonWrapper } from 'components/shared/elements'; -import { useAppSelector } from 'redux/lib/hooks'; -import { - getResourcePermissions, - getTermDetails, - getTermDetailsFetchingStatuses, -} from 'redux/selectors'; import { Permission, PermissionResourceType } from 'generated-sources'; import { WithPermissionsProvider } from 'components/shared/contexts'; import { useTermsRouteParams } from 'routes'; +import { useGetTermByID } from 'lib/hooks'; +import { useResourcePermissions } from 'lib/hooks/api/permissions'; import OverviewGeneral from './OverviewGeneral/OverviewGeneral'; import OverviewSkeleton from './OverviewSkeleton/OverviewSkeleton'; -import OverviewTags from './OverviewTags/OverviewTags'; import * as S from './OverviewStyles'; +import TermLinkedTerms from './TermLinkedTerms/TermLinkedTerms'; +import TermDefinition from './TermDefinition/TermDefinition'; const Overview: FC = () => { const { termId } = useTermsRouteParams(); - const termDetails = useAppSelector(getTermDetails(termId)); - const termPermissions = useAppSelector( - getResourcePermissions(PermissionResourceType.TERM, termId) - ); + const { data: termDetails, isLoading: isTermDetailsFetching } = useGetTermByID({ + termId, + }); + const { data: termPermissions } = useResourcePermissions({ + resourceId: termId, + permissionResourceType: PermissionResourceType.TERM, + }); - const { isLoading: isTermDetailsFetching } = useAppSelector( - getTermDetailsFetchingStatuses + const linkedTerms = useMemo(() => termDetails?.terms || [], [termDetails]); + const termsRef = useMemo( + () => termDetails?.terms?.map(linkedTerm => linkedTerm.term), + [termDetails?.terms] ); return ( @@ -33,10 +35,11 @@ const Overview: FC = () => { - - Definition - - + @@ -47,15 +50,17 @@ const Overview: FC = () => { Permission.TERM_OWNERSHIP_UPDATE, Permission.TERM_OWNERSHIP_DELETE, ]} - resourcePermissions={termPermissions} + resourcePermissions={termPermissions ?? []} Component={OverviewGeneral} /> } + allowedPermissions={[Permission.TERM_UPDATE]} + resourcePermissions={termPermissions ?? []} + render={() => ( + + )} /> diff --git a/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.styles.ts b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.styles.ts new file mode 100644 index 000000000..45607c86b --- /dev/null +++ b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +export const Tooltip = styled('div')(({ theme }) => ({ + fontSize: '14px', + padding: theme.spacing(1), + maxWidth: '430px', + border: '1px solid', + borderRadius: '8px', + borderColor: theme.palette.border.primary, + boxShadow: theme.shadows[9], +})); + +export const Definition = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), +})); \ No newline at end of file diff --git a/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.tsx b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.tsx new file mode 100644 index 000000000..c874d6cd9 --- /dev/null +++ b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermDefinition/TermDefinition.tsx @@ -0,0 +1,53 @@ +import React, { type FC } from 'react'; +import { Box, Grid, Typography } from '@mui/material'; +import { WithPermissions } from 'components/shared/contexts'; +import type { TermRef } from 'generated-sources'; +import { Permission } from 'generated-sources'; +import { AppTooltip, Button, Markdown } from 'components/shared/elements'; +import { useTermWiki } from 'lib/hooks'; +import { updateDataSetFieldDescription } from 'redux/thunks'; +import { InformationIcon, StrokedInfoIcon } from 'components/shared/icons'; +import * as S from './TermDefinition.styles'; + +interface TermDefinitionProps { + termId: number; + definition: string; + terms: TermRef[] | undefined; +} + +const TermDefinition: FC = ({ definition, termId, terms }) => { + const { error, internalDescription, transformDescriptionToMarkdown, editMode } = + useTermWiki({ + terms, + description: definition, + entityId: termId, + updateDescription: updateDataSetFieldDescription, + isDatasetField: false, + }); + + const tooltipInfoContent = ( + + You can link an existing term by entering information about the term according to + the pattern [[NamespaceName:TermName]] +
+
+ Example: This entity describes [[Finance:User]] +
+ ); + + return ( + <> + + + Definition + + + + + + + + ); +}; + +export default TermDefinition; diff --git a/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/LinkedTermTermForm/LinkedTermTermForm.tsx b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/LinkedTermTermForm/LinkedTermTermForm.tsx new file mode 100644 index 000000000..5ebc519e6 --- /dev/null +++ b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/LinkedTermTermForm/LinkedTermTermForm.tsx @@ -0,0 +1,49 @@ +import React, { type FC, useCallback } from 'react'; +import type { LinkedTerm } from 'generated-sources'; +import { useAddLinkedTermToTerm } from 'lib/hooks'; +import { AssignTermForm } from 'components/shared/elements'; + +interface AssignLinkedTermTermFormProps { + openBtnEl: JSX.Element; + termId: number; + handleAddTerm: (linkedTerm: LinkedTerm) => void; +} + +const LinkedTermTermForm: FC = ({ + openBtnEl, + termId, + handleAddTerm, +}) => { + const { + isPending, + isSuccess, + mutateAsync: addTerm, + } = useAddLinkedTermToTerm({ termId }); + + const onSubmit = useCallback( + (clearState: () => void) => + async ({ termId: linkedTermId }: { termId: number }) => { + const linkedTerm = await addTerm({ + termId, + linkedTermFormData: { + linkedTermId, + }, + }); + + handleAddTerm(linkedTerm); + clearState(); + }, + [addTerm, handleAddTerm] + ); + + return ( + + ); +}; + +export default LinkedTermTermForm; diff --git a/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/TermItem/TermItem.tsx b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/TermItem/TermItem.tsx new file mode 100644 index 000000000..f1241942b --- /dev/null +++ b/odd-platform-ui/src/components/Terms/TermDetails/Overview/TermLinkedTerms/TermItem/TermItem.tsx @@ -0,0 +1,78 @@ +import React, { type FC, useCallback } from 'react'; +import { Box, Typography } from '@mui/material'; +import { Permission, type TermRef } from 'generated-sources'; +import { WithPermissions } from 'components/shared/contexts'; +import { + Button, + CollapsibleInfoContainer, + InfoItem, + Markdown, +} from 'components/shared/elements'; +import { DeleteIcon, LinkedTermIcon } from 'components/shared/icons'; +import { useDeleteLinkedTermToTerm } from 'lib/hooks'; +import { termDetailsPath } from 'routes'; +import { Link } from 'react-router-dom'; + +interface TermItemProps { + name: TermRef['name']; + definition: TermRef['definition']; + linkedTermId: TermRef['id']; + termId: number; + isDescriptionLink: boolean; + removeTerm: (linkedTermId: number) => void; +} + +const TermItem: FC = ({ + name, + definition, + linkedTermId, + termId, + removeTerm, + isDescriptionLink, +}) => { + const { mutateAsync: deleteTerm } = useDeleteLinkedTermToTerm({ termId }); + + const termDetailsLink = termDetailsPath(linkedTermId, 'overview'); + + const handleDelete = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + removeTerm(linkedTermId); + }, + [deleteTerm, linkedTermId, removeTerm] + ); + + return ( + + + {name} + + {isDescriptionLink && } + + } + info={ + } + actions={ + !isDescriptionLink ? ( + +