Removed RELATION_QUERY type

This commit is contained in:
dshvaika 2025-10-03 11:43:24 +03:00
parent fcba7004f9
commit eea9e6bf6e
8 changed files with 17 additions and 350 deletions

View File

@ -27,7 +27,6 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
@ -164,21 +163,6 @@ public abstract class AbstractCalculatedFieldProcessingService {
}
var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration();
return switch (refDynamicSourceConfiguration.getType()) {
case RELATION_QUERY -> {
var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
if (configuration.isSimpleRelation()) {
yield switch (configuration.getDirection()) {
case FROM ->
Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
case TO ->
Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
};
}
yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
case RELATION_PATH_QUERY -> {
var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),

View File

@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration;
public enum CFArgumentDynamicSourceType {
RELATION_QUERY, RELATION_PATH_QUERY
RELATION_PATH_QUERY
}

View File

@ -28,7 +28,7 @@ import java.util.List;
import java.util.NoSuchElementException;
@Data
public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased {
public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration {
private List<RelationPathLevel> levels;
@ -53,10 +53,11 @@ public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDy
};
}
@Override
@JsonIgnore
public int getMaxLevel() {
return levels != null ? levels.size() : 0;
public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
if (levels.size() > maxAllowedRelationLevel) {
throw new IllegalArgumentException("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
}
}
public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) {
@ -64,9 +65,6 @@ public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDy
}
private RelationPathLevel getLastLevel() {
if (CollectionsUtil.isEmpty(levels)) {
throw new NoSuchElementException();
}
return levels.get(levels.size() - 1);
}

View File

@ -1,29 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
public interface RelationQueryBased {
int getMaxLevel();
default void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
if (getMaxLevel() > maxAllowedRelationLevel) {
throw new IllegalArgumentException("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
}
}
}

View File

@ -1,79 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
import java.util.List;
@Data
public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased {
private int maxLevel;
private boolean fetchLastLevelOnly;
private EntitySearchDirection direction;
private String relationType;
@Override
public CFArgumentDynamicSourceType getType() {
return CFArgumentDynamicSourceType.RELATION_QUERY;
}
@Override
public void validate() {
if (maxLevel < 1) {
throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!");
}
if (direction == null) {
throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!");
}
if (StringUtils.isBlank(relationType)) {
throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!");
}
}
@JsonIgnore
public boolean isSimpleRelation() {
return maxLevel == 1;
}
public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) {
if (isSimpleRelation()) {
throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!");
}
var entityRelationsQuery = new EntityRelationsQuery();
entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly));
entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList())));
return entityRelationsQuery;
}
public List<EntityId> resolveEntityIds(List<EntityRelation> relations) {
return switch (direction) {
case FROM -> relations.stream().map(EntityRelation::getTo).toList();
case TO -> relations.stream().map(EntityRelation::getFrom).toList();
};
}
}

View File

@ -1,214 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class RelationQueryDynamicSourceConfigurationTest {
@Mock
EntityId rootEntityId;
@Mock
EntityRelation rel1;
@Mock
EntityRelation rel2;
@Test
void typeShouldBeRelationQuery() {
var cfg = new RelationQueryDynamicSourceConfiguration();
assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY);
}
@Test
void validateShouldThrowWhenMaxLevelLessThanOne() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(0);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!");
}
@Test
void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() {
int maxAllowedRelationLevel = 2;
int argumentMaxRelationLevel = 3;
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(argumentMaxRelationLevel);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
String testRelationArgument = "testRelationArgument";
assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument);
}
@Test
void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() {
int maxAllowedRelationLevel = 5;
int argumentMaxRelationLevel = 2;
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(argumentMaxRelationLevel);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
String testRelationArgument = "testRelationArgument";
assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException();
}
@Test
void validateShouldThrowWhenDirectionIsNull() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setDirection(null);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration direction must be specified!");
}
@ParameterizedTest
@ValueSource(strings = {" "})
@NullAndEmptySource
void validateShouldThrowWhenRelationTypeIsNull(String relationType) {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setDirection(EntitySearchDirection.TO);
cfg.setRelationType(relationType);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration relation type must be specified!");
}
@Test
void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
assertThat(cfg.isSimpleRelation()).isTrue();
}
@Test
void isSimpleRelationFalseWhenMaxLevelNotOne() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
assertThat(cfg.isSimpleRelation()).isFalse();
}
@Test
void toEntityRelationsQueryShouldThrowForSimpleRelation() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setFetchLastLevelOnly(false);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Entity relations query can't be created for a simple relation!");
}
@Test
void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
cfg.setFetchLastLevelOnly(true);
cfg.setDirection(EntitySearchDirection.TO);
cfg.setRelationType(EntityRelation.MANAGES_TYPE);
var query = cfg.toEntityRelationsQuery(rootEntityId);
assertThat(query).isNotNull();
RelationsSearchParameters params = query.getParameters();
assertThat(params).isNotNull();
assertThat(params.getRootId()).isEqualTo(rootEntityId.getId());
assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO);
assertThat(params.getMaxLevel()).isEqualTo(2);
assertThat(params.isFetchLastLevelOnly()).isTrue();
assertThat(query.getFilters()).hasSize(1);
assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class);
RelationEntityTypeFilter filter = query.getFilters().get(0);
assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE);
}
@Test
void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() {
when(rel1.getTo()).thenReturn(mock(EntityId.class));
when(rel2.getTo()).thenReturn(mock(EntityId.class));
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setDirection(EntitySearchDirection.FROM);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getTo(), rel2.getTo());
}
@Test
void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() {
when(rel1.getFrom()).thenReturn(mock(EntityId.class));
when(rel2.getFrom()).thenReturn(mock(EntityId.class));
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setDirection(EntitySearchDirection.TO);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom());
}
@Test
void validateShouldPassForValidConfig() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
cfg.setFetchLastLevelOnly(false);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatCode(cfg::validate).doesNotThrowAnyException();
}
}

View File

@ -504,6 +504,13 @@ public class BaseRelationService implements RelationService {
log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery);
validateId(tenantId, id -> "Invalid tenant id: " + id);
validate(relationPathQuery);
if (relationPathQuery.levels().size() == 1) {
RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0);
return switch (relationPathLevel.direction()) {
case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
};
}
return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery));
}

View File

@ -19,7 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryBased;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -98,10 +98,10 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) {
return;
}
Map<String, RelationQueryBased> relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet()
Map<String, RelationPathQueryDynamicSourceConfiguration> relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet()
.stream()
.filter(entry -> entry.getValue().hasDynamicSource())
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryBased) entry.getValue().getRefDynamicSourceConfiguration()));
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration()));
if (relationQueryBasedArguments.isEmpty()) {
return;
}