Browse Source

Move OR validation to BaseEntityService and fix toggle behavior

- Move validateKeyFiltersOperation from DefaultEntityQueryService to
  BaseEntityService.validateEntityCountQuery so both REST and WebSocket
  paths are covered
- Fix AND/OR toggle: use valueChanges listener to prevent OR selection
  when disabled (instead of [disabled] which broke the control entirely)
- Force AND when OR is disabled and filter had OR saved
- Update test to set flag on BaseEntityService
pull/15394/head
Viacheslav Klimov 6 months ago
parent
commit
d1566449d0
Failed to extract signature
  1. 21
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  2. 7
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  3. 14
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  4. 1
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  5. 11
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts

21
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -18,7 +18,7 @@ package org.thingsboard.server.service.query;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexOperation;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
@ -95,10 +94,6 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Value("${server.ws.max_entities_per_alarm_subscription:1000}") @Value("${server.ws.max_entities_per_alarm_subscription:1000}")
private int maxEntitiesPerAlarmSubscription; private int maxEntitiesPerAlarmSubscription;
@Setter
@Value("${sql.query.key-filters-or-conditions.enabled:true}")
private boolean keyFiltersOrConditionsEnabled;
@Autowired @Autowired
private DbCallbackExecutorService dbCallbackExecutor; private DbCallbackExecutorService dbCallbackExecutor;
@ -110,13 +105,13 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override @Override
public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) {
validateKeyFiltersOperation(query);
return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
} }
@Override @Override
public PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { public PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) {
validateKeyFiltersOperation(query);
if (query.getKeyFilters() != null) { if (query.getKeyFilters() != null) {
resolveDynamicValuesInPredicates( resolveDynamicValuesInPredicates(
query.getKeyFilters().stream() query.getKeyFilters().stream()
@ -186,7 +181,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override @Override
public PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { public PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) {
validateKeyFiltersOperation(query);
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);
PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(),
securityUser.getCustomerId(), entityDataQuery); securityUser.getCustomerId(), entityDataQuery);
@ -213,7 +208,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override @Override
public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) {
validateKeyFiltersOperation(query);
if (query.getEntityFilter() != null) { if (query.getEntityFilter() != null) {
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);
PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(),
@ -246,11 +241,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault());
} }
private void validateKeyFiltersOperation(EntityCountQuery query) {
if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) {
throw new IllegalArgumentException("OR conditions between key filters are disabled");
}
}
@Override @Override
public ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, public ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,

7
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -90,6 +90,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb; import org.thingsboard.server.edqs.util.EdqsRocksDb;
@ -127,6 +128,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
private QueueStatsService queueStatsService; private QueueStatsService queueStatsService;
@Autowired @Autowired
private DefaultEntityQueryService entityQueryService; private DefaultEntityQueryService entityQueryService;
@Autowired
private BaseEntityService baseEntityService;
@MockitoBean @MockitoBean
private EdqsRocksDb edqsRocksDb; private EdqsRocksDb edqsRocksDb;
@ -2495,7 +2498,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
entityQueryService.setKeyFiltersOrConditionsEnabled(false); baseEntityService.setKeyFiltersOrConditionsEnabled(false);
try { try {
DeviceTypeFilter filter = new DeviceTypeFilter(); DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("default")); filter.setDeviceTypes(List.of("default"));
@ -2516,7 +2519,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND);
doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk());
} finally { } finally {
entityQueryService.setKeyFiltersOrConditionsEnabled(true); baseEntityService.setKeyFiltersOrConditionsEnabled(true);
} }
} }

14
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -19,8 +19,10 @@ import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.ComplexOperation;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -91,6 +94,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
private static final Set<EntityFilterType> EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( private static final Set<EntityFilterType> EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of(
EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY);
@Setter
@Value("${sql.query.key-filters-or-conditions.enabled:true}")
private boolean keyFiltersOrConditionsEnabled;
@Autowired @Autowired
private EntityQueryDao entityQueryDao; private EntityQueryDao entityQueryDao;
@ -328,7 +335,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity)); return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity));
} }
private static void validateEntityCountQuery(EntityCountQuery query) { private void validateEntityCountQuery(EntityCountQuery query) {
if (query == null) { if (query == null) {
throw new IncorrectParameterException("Query must be specified."); throw new IncorrectParameterException("Query must be specified.");
} else if (query.getEntityFilter() == null) { } else if (query.getEntityFilter() == null) {
@ -342,9 +349,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
} else if (query.getEntityFilter().getType().equals(ENTITY_NAME)) { } else if (query.getEntityFilter().getType().equals(ENTITY_NAME)) {
validateEntityNameQuery((EntityNameFilter) query.getEntityFilter()); validateEntityNameQuery((EntityNameFilter) query.getEntityFilter());
} }
if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) {
throw new IncorrectParameterException("OR conditions between key filters are disabled");
}
} }
private static void validateEntityDataQuery(EntityDataQuery query) { private void validateEntityDataQuery(EntityDataQuery query) {
validateEntityCountQuery(query); validateEntityCountQuery(query);
validateEntityDataPageLink(query.getPageLink()); validateEntityDataPageLink(query.getPageLink());
} }

1
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -51,7 +51,6 @@
<div class="flex flex-row items-center justify-between" style="margin-top: 8px;"> <div class="flex flex-row items-center justify-between" style="margin-top: 8px;">
<div class="tb-form-panel-title" translate>filter.key-filters</div> <div class="tb-form-panel-title" translate>filter.key-filters</div>
<tb-toggle-select formControlName="keyFiltersOperation" <tb-toggle-select formControlName="keyFiltersOperation"
[disabled]="!allowKeyFiltersOrConditions"
[matTooltip]="!allowKeyFiltersOrConditions ? ('filter.or-conditions-disabled' | translate) : ''" [matTooltip]="!allowKeyFiltersOrConditions ? ('filter.or-conditions-disabled' | translate) : ''"
selectMediaBreakpoint="xs"> selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option> <tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>

11
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts

@ -94,12 +94,21 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
} }
this.allowKeyFiltersOrConditions = getCurrentAuthState(this.store).allowKeyFiltersOrConditions !== false; this.allowKeyFiltersOrConditions = getCurrentAuthState(this.store).allowKeyFiltersOrConditions !== false;
const initialOperation = !this.allowKeyFiltersOrConditions ? ComplexOperation.AND
: (this.filter.keyFiltersOperation ?? ComplexOperation.AND);
this.filterFormGroup = this.fb.group({ this.filterFormGroup = this.fb.group({
filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]], filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]],
editable: [this.filter.editable], editable: [this.filter.editable],
keyFilters: [this.filter.keyFilters, Validators.required], keyFilters: [this.filter.keyFilters, Validators.required],
keyFiltersOperation: [this.filter.keyFiltersOperation ?? ComplexOperation.AND] keyFiltersOperation: [initialOperation]
}); });
if (!this.allowKeyFiltersOrConditions) {
this.filterFormGroup.get('keyFiltersOperation').valueChanges.subscribe(value => {
if (value === ComplexOperation.OR) {
this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND, {emitEvent: false});
}
});
}
} }
validateDuplicateFilterName(): ValidatorFn { validateDuplicateFilterName(): ValidatorFn {

Loading…
Cancel
Save