Browse Source

Merge pull request #12009 from irynamatveieva/calclulated-fields

Calculated fields
pull/12373/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
4af4a1eeaf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  4. 46
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  5. 43
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/CalculatedFieldsDeletionTaskProcessor.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  8. 41
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  9. 16
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  10. 10
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  11. 43
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldConfig.java
  12. 11
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java
  13. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
  14. 3
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java
  15. 4
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  16. 82
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  17. 117
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldConfigUtil.java
  18. 15
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java
  19. 10
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  21. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  22. 5
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  23. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java
  24. 1
      dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java
  25. 8
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
  26. 8
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  27. 4
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
  28. 4
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java
  29. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java
  30. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  31. 34
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  32. 18
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java
  33. 109
      dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
  34. 61
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  35. 57
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java
  36. 44
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  37. 5
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java
  38. 4
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java
  39. 6
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java
  40. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  41. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java
  42. 17
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -62,6 +62,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.ClaimDevicesService;
@ -389,6 +390,10 @@ public class ActorSystemContext {
@Getter
private SlackService slackService;
@Autowired
@Getter
private CalculatedFieldService calculatedFieldService;
@Lazy
@Autowired(required = false)
@Getter

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -78,6 +78,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -848,6 +849,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSlackService();
}
@Override
public CalculatedFieldService getCalculatedFieldService() {
return mainCtx.getCalculatedFieldService();
}
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();

4
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -67,7 +67,7 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInfo;
@ -126,7 +126,7 @@ import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.ClaimDevicesService;

46
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -27,14 +27,22 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@ -46,6 +54,9 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@Slf4j
public class CalculatedFieldController extends BaseController {
private static final Set<EntityType> supportedEntityTypesForReferencedEntities = EnumSet.of(
EntityType.TENANT, EntityType.CUSTOMER, EntityType.ASSET, EntityType.DEVICE);
private final CalculatedFieldService calculatedFieldService;
public static final String CALCULATED_FIELD_ID = "calculatedFieldId";
@ -57,26 +68,30 @@ public class CalculatedFieldController extends BaseController {
"Referencing non-existing Calculated Field Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. "
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField", method = RequestMethod.POST)
@ResponseBody
public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.")
@RequestBody CalculatedField calculatedField) throws Exception {
calculatedField.setTenantId(getTenantId());
checkEntity(calculatedField.getId(), calculatedField, Resource.CALCULATED_FIELD);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField.getConfiguration());
return calculatedFieldService.save(calculatedField);
}
@ApiOperation(value = "Get Calculated Field (getCalculatedFieldById)",
notes = "Fetch the Calculated Field object based on the provided Calculated Field Id."
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.GET)
@ResponseBody
public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
return checkCalculatedFieldId(calculatedFieldId, Operation.READ);
CalculatedField calculatedField = calculatedFieldService.findById(getTenantId(), calculatedFieldId);
checkNotNull(calculatedField);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
return calculatedField;
}
@ -88,8 +103,23 @@ public class CalculatedFieldController extends BaseController {
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedField) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedField);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedField));
checkCalculatedFieldId(calculatedFieldId, Operation.DELETE);
TenantId tenantId = getTenantId();
CalculatedField calculatedField = calculatedFieldService.findById(tenantId, calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
calculatedFieldService.deleteCalculatedField(getTenantId(), calculatedFieldId);
}
private void checkReferencedEntities(CalculatedFieldConfig calculatedFieldConfig) throws ThingsboardException {
List<EntityId> referencedEntityIds = calculatedFieldConfig.getArguments().values().stream()
.map(CalculatedFieldConfig.Argument::getEntityId)
.filter(Objects::nonNull)
.toList();
for (EntityId referencedEntityId : referencedEntityIds) {
if (!supportedEntityTypesForReferencedEntities.contains(referencedEntityId.getEntityType())) {
throw new IllegalArgumentException("Calculated fields do not support entity type '" + referencedEntityId.getEntityType() + "' for referenced entities.");
}
checkEntityId(referencedEntityId, Operation.READ);
}
}
}

43
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/CalculatedFieldsDeletionTaskProcessor.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2024 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.service.housekeeper.processor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
@Component
@RequiredArgsConstructor
@Slf4j
public class CalculatedFieldsDeletionTaskProcessor extends HousekeeperTaskProcessor<HousekeeperTask> {
private final CalculatedFieldService calculatedFieldService;
@Override
public void process(HousekeeperTask task) throws Exception {
int deletedCount = calculatedFieldService.deleteAllCalculatedFieldsByEntityId(task.getTenantId(), task.getEntityId());
log.debug("[{}][{}][{}] Deleted {} calculated fields", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount);
}
@Override
public HousekeeperTaskType getTaskType() {
return HousekeeperTaskType.DELETE_CALCULATED_FIELDS;
}
}

2
application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java

@ -19,6 +19,6 @@ public enum Operation {
ALL, CREATE, READ, WRITE, DELETE, ASSIGN_TO_CUSTOMER, UNASSIGN_FROM_CUSTOMER, RPC_CALL,
READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES,
ASSIGN_TO_TENANT
ASSIGN_TO_TENANT, READ_CALCULATED_FIELD, WRITE_CALCULATED_FIELD
}

4
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value="tenantAdminPermissions")
@Component(value = "tenantAdminPermissions")
public class TenantAdminPermissions extends AbstractPermissions {
public TenantAdminPermissions() {
@ -51,14 +51,12 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@Override
public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) {
if (!user.getTenantId().equals(entity.getTenantId())) {
return false;
}

41
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -18,15 +18,18 @@ package org.thingsboard.server.controller;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -75,7 +78,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId()));
assertThat(savedCalculatedField.getVersion()).isEqualTo(calculatedField.getVersion());
savedCalculatedField.setName("Test CF");
@ -115,7 +118,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());
doGet("/api/calculatedField/" + savedCalculatedField.getId().getId()).andExpect(status().isNotFound());
}
private CalculatedField getCalculatedField(DeviceId deviceId) {
@ -124,19 +126,28 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType("Simple");
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(JacksonUtil.toJsonNode("{\n" +
" \"T\": {\n" +
" \"key\": \"temperature\",\n" +
" \"type\": \"TIME_SERIES\"\n" +
" },\n" +
" \"H\": {\n" +
" \"key\": \"humidity\",\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"defaultValue\": 50\n" +
" }\n" +
" }\n"));
calculatedField.setConfiguration(getCalculatedFieldConfig(null));
calculatedField.setVersion(1L);
return calculatedField;
}
private CalculatedFieldConfig getCalculatedFieldConfig(EntityId referencedEntityId) {
CalculatedFieldConfig config = new CalculatedFieldConfig();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
argument.setEntityId(referencedEntityId);
argument.setType("TIME_SERIES");
argument.setKey("temperature");
config.setArguments(Map.of("T", argument));
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType("TIME_SERIES");
output.setExpression("T - (100 - H) / 5");
config.setOutput(output);
return config;
}
}

16
common/dao-api/src/main/java/org/thingsboard/server/dao/calculated_field/CalculatedFieldService.java → common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.calculated_field;
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.entity.EntityDaoService;
@ -29,6 +31,14 @@ public interface CalculatedFieldService extends EntityDaoService {
void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId);
int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);
CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink);
CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId);
boolean existsByEntityId(TenantId tenantId, EntityId entityId);
boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId);
}

10
common/data/src/main/java/org/thingsboard/server/common/data/calculated_field/CalculatedField.java → common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.calculated_field;
package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -47,8 +46,8 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
private String name;
@Schema(description = "Version of calculated field configuration.", example = "0")
private int configurationVersion;
@Schema(description = "JSON with the calculated field configuration.", implementation = com.fasterxml.jackson.databind.JsonNode.class)
private transient JsonNode configuration;
@Schema
private transient CalculatedFieldConfig configuration;
@Getter
@Setter
private Long version;
@ -64,8 +63,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
super(id);
}
public CalculatedField(TenantId tenantId, EntityId entityId, String type, String name, int configurationVersion, JsonNode configuration, Long version, CalculatedFieldId externalId) {
super();
public CalculatedField(TenantId tenantId, EntityId entityId, String type, String name, int configurationVersion, CalculatedFieldConfig configuration, Long version, CalculatedFieldId externalId) {
this.tenantId = tenantId;
this.entityId = entityId;
this.type = type;

43
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldConfig.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2024 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;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@Data
public class CalculatedFieldConfig {
private Map<String, Argument> arguments;
private Output output;
@Data
public static class Argument {
private EntityId entityId;
private String key;
private String type;
private int defaultValue;
}
@Data
public static class Output {
private String type;
private String expression;
}
}

11
common/data/src/main/java/org/thingsboard/server/common/data/calculated_field/CalculatedFieldLink.java → common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java

@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.calculated_field;
package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -37,8 +36,8 @@ public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
@Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY)
private CalculatedFieldId calculatedFieldId;
@Schema(description = "JSON with the calculated field link configuration.", implementation = com.fasterxml.jackson.databind.JsonNode.class)
private transient JsonNode configuration;
@Schema
private transient CalculatedFieldConfig configuration;
public CalculatedFieldLink() {
super();
@ -48,11 +47,11 @@ public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
super(id);
}
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, JsonNode configuration, CalculatedFieldId calculatedFieldId) {
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId, CalculatedFieldConfig configuration) {
this.tenantId = tenantId;
this.entityId = entityId;
this.configuration = configuration;
this.calculatedFieldId = calculatedFieldId;
this.configuration = configuration;
}
@Override

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java

@ -81,6 +81,10 @@ public class HousekeeperTask implements Serializable {
return new TenantEntitiesDeletionHousekeeperTask(tenantId, entityType);
}
public static HousekeeperTask deleteCalculatedFields(TenantId tenantId, EntityId entityId) {
return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_CALCULATED_FIELDS);
}
@JsonIgnore
public String getDescription() {
return taskType.getDescription() + " for " + entityId.getEntityType().getNormalName().toLowerCase() + " " + entityId.getId();

3
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java

@ -30,7 +30,8 @@ public enum HousekeeperTaskType {
DELETE_ALARMS("alarms deletion"),
UNASSIGN_ALARMS("alarms unassigning"),
DELETE_TENANT_ENTITIES("tenant entities deletion"),
DELETE_ENTITIES("entities deletion");
DELETE_ENTITIES("entities deletion"),
DELETE_CALCULATED_FIELDS("calculated fields deletion");
private final String description;

4
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -222,8 +222,8 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
if (!force && entityViewService.existsByTenantIdAndEntityId(tenantId, id)) {
throw new DataValidationException("Can't delete asset that has entity views!");
if (!force && (entityViewService.existsByTenantIdAndEntityId(tenantId, id) || calculatedFieldService.referencedInAnyCalculatedField(tenantId, id))) {
throw new DataValidationException("Can't delete asset that has entity views or is referenced in calculated fields!");
}
Asset asset = assetDao.findById(tenantId, id.getId());

82
dao/src/main/java/org/thingsboard/server/dao/calculated_field/BaseCalculatedFieldService.java → dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -13,25 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.calculated_field;
package org.thingsboard.server.dao.cf;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Objects;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation;
@ -49,6 +55,8 @@ public class BaseCalculatedFieldService implements CalculatedFieldService {
private final CalculatedFieldLinkDao calculatedFieldLinkDao;
private final DeviceService deviceService;
private final AssetService assetService;
private final DeviceProfileService deviceProfileService;
private final AssetProfileService assetProfileService;
private final DataValidator<CalculatedField> calculatedFieldDataValidator;
private final DataValidator<CalculatedFieldLink> calculatedFieldLinkDataValidator;
@ -72,7 +80,7 @@ public class BaseCalculatedFieldService implements CalculatedFieldService {
@Override
public CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findById, tenantId [{}], rpcId [{}]", tenantId, calculatedFieldId);
log.trace("Executing findById, tenantId [{}], calculatedFieldId [{}]", tenantId, calculatedFieldId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldId, id -> INCORRECT_CALCULATED_FIELD_ID + id);
return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId());
@ -80,12 +88,21 @@ public class BaseCalculatedFieldService implements CalculatedFieldService {
@Override
public void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing deleteRpc, tenantId [{}], rpcId [{}]", tenantId, calculatedFieldId);
log.trace("Executing deleteCalculatedField, tenantId [{}], calculatedFieldId [{}]", tenantId, calculatedFieldId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldId, id -> INCORRECT_CALCULATED_FIELD_ID + id);
calculatedFieldDao.removeById(tenantId, calculatedFieldId.getId());
}
@Override
public int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteAllCalculatedFieldsByEntityId, tenantId [{}], entityId [{}]", tenantId, entityId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(entityId.getId(), id -> "Incorrect entityId " + id);
List<CalculatedField> calculatedFields = calculatedFieldDao.removeAllByEntityId(tenantId, entityId);
return calculatedFields.size();
}
@Override
public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) {
calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId);
@ -98,6 +115,29 @@ public class BaseCalculatedFieldService implements CalculatedFieldService {
}
}
@Override
public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) {
log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id);
return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId());
}
@Override
public boolean existsByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldDao.existsByTenantIdAndEntityId(tenantId, entityId);
}
@Override
public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) {
return calculatedFieldDao.findAllByTenantId(tenantId).stream()
.filter(calculatedField -> !referencedEntityId.equals(calculatedField.getEntityId()))
.map(CalculatedField::getConfiguration)
.map(CalculatedFieldConfig::getArguments)
.flatMap(arguments -> arguments.values().stream())
.anyMatch(argument -> referencedEntityId.equals(argument.getEntityId()));
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findById(tenantId, new CalculatedFieldId(entityId.getId())));
@ -114,23 +154,33 @@ public class BaseCalculatedFieldService implements CalculatedFieldService {
.orElseThrow(() -> new IllegalArgumentException("Asset with id [" + entityId.getId() + "] does not exist."));
case DEVICE -> Optional.ofNullable(deviceService.findDeviceById(tenantId, (DeviceId) entityId))
.orElseThrow(() -> new IllegalArgumentException("Device with id [" + entityId.getId() + "] does not exist."));
case ASSET_PROFILE ->
Optional.ofNullable(assetProfileService.findAssetProfileById(tenantId, (AssetProfileId) entityId))
.orElseThrow(() -> new IllegalArgumentException("Asset Profile with id [" + entityId.getId() + "] does not exist."));
case DEVICE_PROFILE ->
Optional.ofNullable(deviceProfileService.findDeviceProfileById(tenantId, (DeviceProfileId) entityId))
.orElseThrow(() -> new IllegalArgumentException("Device Profile with id [" + entityId.getId() + "] does not exist."));
default ->
throw new IllegalArgumentException("Entity type '" + entityId.getEntityType() + "' is not supported.");
throw new IllegalArgumentException("Entity type '" + entityId.getEntityType() + "' does not support calculated fields.");
}
}
private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) {
CalculatedFieldLink calculatedFieldLink = calculatedFieldLinkDao.findCalculatedFieldLinkByEntityId(tenantId.getId(), calculatedField.getEntityId().getId());
saveCalculatedFieldLink(tenantId, Objects.requireNonNullElseGet(calculatedFieldLink, () -> createCalculatedFieldLink(tenantId, calculatedField)));
CalculatedFieldLink existingLink = (calculatedField.getId() != null)
? calculatedFieldLinkDao.findCalculatedFieldLinkByCalculatedFieldId(tenantId, calculatedField.getId())
: null;
CalculatedFieldLink updatedLink = buildCalculatedFieldLink(tenantId, calculatedField, existingLink);
saveCalculatedFieldLink(tenantId, updatedLink);
}
private CalculatedFieldLink createCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setTenantId(tenantId);
calculatedFieldLink.setEntityId(calculatedField.getEntityId());
calculatedFieldLink.setCalculatedFieldId(calculatedField.getId());
calculatedFieldLink.setConfiguration(calculatedField.getConfiguration());
return calculatedFieldLink;
private CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField, CalculatedFieldLink existingLink) {
CalculatedFieldLink link = (existingLink != null) ? existingLink : new CalculatedFieldLink();
link.setTenantId(tenantId);
link.setEntityId(calculatedField.getEntityId());
link.setCalculatedFieldId(calculatedField.getId());
link.setConfiguration(calculatedField.getConfiguration());
return link;
}
}

117
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldConfigUtil.java

@ -0,0 +1,117 @@
/**
* Copyright © 2016-2024 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.dao.cf;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class CalculatedFieldConfigUtil {
public static CalculatedFieldConfig toCalculatedFieldConfig(JsonNode config, EntityType entityType, UUID entityId) {
if (config == null) {
return null;
}
try {
CalculatedFieldConfig calculatedFieldConfig = new CalculatedFieldConfig();
Map<String, CalculatedFieldConfig.Argument> arguments = new HashMap<>();
JsonNode argumentsNode = config.get("arguments");
if (argumentsNode != null && argumentsNode.isObject()) {
argumentsNode.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode argumentNode = entry.getValue();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
if (argumentNode.has("entityType") && argumentNode.has("entityId")) {
String referencedEntityType = argumentNode.get("entityType").asText();
UUID referencedEntityId = UUID.fromString(argumentNode.get("entityId").asText());
argument.setEntityId(EntityIdFactory.getByTypeAndUuid(referencedEntityType, referencedEntityId));
} else {
argument.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
}
argument.setKey(argumentNode.get("key").asText());
argument.setType(argumentNode.get("type").asText());
if (argumentNode.has("defaultValue")) {
argument.setDefaultValue(argumentNode.get("defaultValue").asInt());
}
arguments.put(key, argument);
});
}
calculatedFieldConfig.setArguments(arguments);
JsonNode outputNode = config.get("output");
if (outputNode != null) {
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType(outputNode.get("type").asText());
output.setExpression(outputNode.get("expression").asText());
calculatedFieldConfig.setOutput(output);
}
return calculatedFieldConfig;
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert JsonNode to CalculatedFieldConfig", e);
}
}
public static JsonNode calculatedFieldConfigToJson(CalculatedFieldConfig calculatedFieldConfig, EntityType entityType, UUID entityId) {
if (calculatedFieldConfig == null) {
return null;
}
try {
ObjectNode configNode = JacksonUtil.newObjectNode();
ObjectNode argumentsNode = configNode.putObject("arguments");
calculatedFieldConfig.getArguments().forEach((key, argument) -> {
ObjectNode argumentNode = argumentsNode.putObject(key);
EntityId referencedEntityId = argument.getEntityId();
if (referencedEntityId != null) {
argumentNode.put("entityType", referencedEntityId.getEntityType().name());
argumentNode.put("entityId", referencedEntityId.getId().toString());
} else {
argumentNode.put("entityType", entityType.name());
argumentNode.put("entityId", entityId.toString());
}
argumentNode.put("key", argument.getKey());
argumentNode.put("type", argument.getType());
argumentNode.put("defaultValue", argument.getDefaultValue());
});
if (calculatedFieldConfig.getOutput() != null) {
ObjectNode outputNode = configNode.putObject("output");
outputNode.put("type", calculatedFieldConfig.getOutput().getType());
outputNode.put("expression", calculatedFieldConfig.getOutput().getExpression());
}
return configNode;
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert CalculatedFieldConfig to JsonNode", e);
}
}
}

15
dao/src/main/java/org/thingsboard/server/dao/calculated_field/CalculatedFieldDao.java → dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java

@ -13,10 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.calculated_field;
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.Dao;
import java.util.List;
public interface CalculatedFieldDao extends Dao<CalculatedField> {
boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedField> findAllByTenantId(TenantId tenantId);
List<CalculatedField> removeAllByEntityId(TenantId tenantId, EntityId entityId);
}

10
dao/src/main/java/org/thingsboard/server/dao/calculated_field/CalculatedFieldLinkDao.java → dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java

@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.calculated_field;
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.Dao;
import java.util.UUID;
public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> {
CalculatedFieldLink findCalculatedFieldLinkByEntityId(UUID tenantId, UUID entityId);
CalculatedFieldLink findCalculatedFieldLinkByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId);
}

4
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -184,6 +184,10 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Transactional
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
if (!force && calculatedFieldService.referencedInAnyCalculatedField(tenantId, id)) {
throw new DataValidationException("Can't delete customer that is referenced in calculated fields!");
}
CustomerId customerId = (CustomerId) id;
Customer customer = findCustomerById(tenantId, customerId);
if (customer == null) {

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -337,8 +337,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
if (!force && entityViewService.existsByTenantIdAndEntityId(tenantId, id)) {
throw new DataValidationException("Can't delete device that has entity views!");
if (!force && (entityViewService.existsByTenantIdAndEntityId(tenantId, id) || calculatedFieldService.referencedInAnyCalculatedField(tenantId, id))) {
throw new DataValidationException("Can't delete device that has entity views or is referenced in calculated fields!");
}
Device device = deviceDao.findById(tenantId, id.getId());

5
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -60,6 +61,10 @@ public abstract class AbstractEntityService {
@Autowired
protected EntityViewService entityViewService;
@Lazy
@Autowired
protected CalculatedFieldService calculatedFieldService;
@Lazy
@Autowired(required = false)
protected EdgeService edgeService;

3
dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java

@ -43,6 +43,9 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry {
if (EntityType.RULE_CHAIN.equals(entityType)) {
entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService);
}
if (EntityType.CALCULATED_FIELD.equals(entityType)) {
entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService);
}
});
log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size());
}

1
dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java

@ -75,6 +75,7 @@ public class CleanUpService {
submitTask(HousekeeperTask.deleteTelemetry(tenantId, entityId));
submitTask(HousekeeperTask.deleteEvents(tenantId, entityId));
submitTask(HousekeeperTask.deleteAlarms(tenantId, entityId));
submitTask(HousekeeperTask.deleteCalculatedFields(tenantId, entityId));
}
public void removeTenantEntities(TenantId tenantId, EntityType... entityTypes) {

8
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java

@ -23,7 +23,7 @@ import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
@ -33,6 +33,8 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
import static org.thingsboard.server.dao.cf.CalculatedFieldConfigUtil.calculatedFieldConfigToJson;
import static org.thingsboard.server.dao.cf.CalculatedFieldConfigUtil.toCalculatedFieldConfig;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
@ -91,7 +93,7 @@ public class CalculatedFieldEntity extends BaseSqlEntity<CalculatedField> implem
this.type = calculatedField.getType();
this.name = calculatedField.getName();
this.configurationVersion = calculatedField.getConfigurationVersion();
this.configuration = calculatedField.getConfiguration();
this.configuration = calculatedFieldConfigToJson(calculatedField.getConfiguration(), entityType, entityId);
this.version = calculatedField.getVersion();
if (calculatedField.getExternalId() != null) {
this.externalId = calculatedField.getExternalId().getId();
@ -107,7 +109,7 @@ public class CalculatedFieldEntity extends BaseSqlEntity<CalculatedField> implem
calculatedField.setType(type);
calculatedField.setName(name);
calculatedField.setConfigurationVersion(configurationVersion);
calculatedField.setConfiguration(configuration);
calculatedField.setConfiguration(toCalculatedFieldConfig(configuration, entityType, entityId));
calculatedField.setVersion(version);
if (externalId != null) {
calculatedField.setExternalId(new CalculatedFieldId(externalId));

8
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java

@ -23,7 +23,7 @@ import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -34,6 +34,8 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
import static org.thingsboard.server.dao.cf.CalculatedFieldConfigUtil.calculatedFieldConfigToJson;
import static org.thingsboard.server.dao.cf.CalculatedFieldConfigUtil.toCalculatedFieldConfig;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID;
@ -75,7 +77,7 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
this.entityType = calculatedFieldLink.getEntityId().getEntityType();
this.entityId = calculatedFieldLink.getEntityId().getId();
this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId();
this.configuration = calculatedFieldLink.getConfiguration();
this.configuration = calculatedFieldConfigToJson(calculatedFieldLink.getConfiguration(), entityType, entityId);
}
@Override
@ -85,7 +87,7 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
calculatedFieldLink.setConfiguration(configuration);
calculatedFieldLink.setConfiguration(toCalculatedFieldConfig(configuration, entityType, entityId));
return calculatedFieldLink;
}

4
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java

@ -17,9 +17,9 @@ package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldDao;
import org.thingsboard.server.dao.cf.CalculatedFieldDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;

4
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java

@ -17,9 +17,9 @@ package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;

4
dao/src/main/java/org/thingsboard/server/dao/sql/calculated_field/CalculatedFieldLinkRepository.java → dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.calculated_field;
package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
@ -22,6 +22,6 @@ import java.util.UUID;
public interface CalculatedFieldLinkRepository extends JpaRepository<CalculatedFieldLinkEntity, UUID> {
CalculatedFieldLinkEntity findByTenantIdAndEntityId(UUID tenantId, UUID entityId);
CalculatedFieldLinkEntity findByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId);
}

10
dao/src/main/java/org/thingsboard/server/dao/sql/calculated_field/CalculatedFieldRepository.java → dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -13,12 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.calculated_field;
package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
import java.util.List;
import java.util.UUID;
public interface CalculatedFieldRepository extends JpaRepository<CalculatedFieldEntity, UUID> {
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
List<CalculatedFieldEntity> findAllByTenantId(UUID tenantId);
List<CalculatedFieldEntity> removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
}

34
dao/src/main/java/org/thingsboard/server/dao/sql/calculated_field/JpaCalculatedFieldDao.java → dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -13,18 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.calculated_field;
package org.thingsboard.server.dao.sql.cf;
import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldDao;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.cf.CalculatedFieldDao;
import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
@Slf4j
@ -35,6 +41,22 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
private final CalculatedFieldRepository calculatedFieldRepository;
@Override
public boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldRepository.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
}
@Override
public List<CalculatedField> findAllByTenantId(TenantId tenantId) {
return DaoUtil.convertDataList(calculatedFieldRepository.findAllByTenantId(tenantId.getId()));
}
@Override
@Transactional
public List<CalculatedField> removeAllByEntityId(TenantId tenantId, EntityId entityId) {
return DaoUtil.convertDataList(calculatedFieldRepository.removeAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
protected Class<CalculatedFieldEntity> getEntityClass() {
return CalculatedFieldEntity.class;
@ -44,4 +66,10 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
protected JpaRepository<CalculatedFieldEntity, UUID> getRepository() {
return calculatedFieldRepository;
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
}
}

18
dao/src/main/java/org/thingsboard/server/dao/sql/calculated_field/JpaCalculatedFieldLinkDao.java → dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java

@ -13,15 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.calculated_field;
package org.thingsboard.server.dao.sql.cf;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
@ -37,8 +40,8 @@ public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLin
private final CalculatedFieldLinkRepository calculatedFieldLinkRepository;
@Override
public CalculatedFieldLink findCalculatedFieldLinkByEntityId(UUID tenantId, UUID entityId) {
return DaoUtil.getData(calculatedFieldLinkRepository.findByTenantIdAndEntityId(tenantId, entityId));
public CalculatedFieldLink findCalculatedFieldLinkByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
return DaoUtil.getData(calculatedFieldLinkRepository.findByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId()));
}
@Override
@ -51,4 +54,9 @@ public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLin
return calculatedFieldLinkRepository;
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD_LINK;
}
}

109
dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java

@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -39,6 +41,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.asset.AssetDao;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
@ -46,7 +49,9 @@ import org.thingsboard.server.dao.relation.RelationService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@DaoSqlTest
@ -63,6 +68,8 @@ public class AssetServiceTest extends AbstractServiceTest {
@Autowired
private AssetProfileService assetProfileService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private PlatformTransactionManager platformTransactionManager;
private IdComparator<Asset> idComparator = new IdComparator<>();
@ -214,24 +221,24 @@ public class AssetServiceTest extends AbstractServiceTest {
public void testFindAssetTypesByTenantId() throws Exception {
List<Asset> assets = new ArrayList<>();
try {
for (int i=0;i<3;i++) {
for (int i = 0; i < 3; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("My asset B"+i);
asset.setName("My asset B" + i);
asset.setType("typeB");
assets.add(assetService.saveAsset(asset));
}
for (int i=0;i<7;i++) {
for (int i = 0; i < 7; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("My asset C"+i);
asset.setName("My asset C" + i);
asset.setType("typeC");
assets.add(assetService.saveAsset(asset));
}
for (int i=0;i<9;i++) {
for (int i = 0; i < 9; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("My asset A"+i);
asset.setName("My asset A" + i);
asset.setType("typeA");
assets.add(assetService.saveAsset(asset));
}
@ -242,7 +249,9 @@ public class AssetServiceTest extends AbstractServiceTest {
Assert.assertEquals("typeB", assetTypes.get(1).getType());
Assert.assertEquals("typeC", assetTypes.get(2).getType());
} finally {
assets.forEach((asset) -> { assetService.deleteAsset(tenantId, asset.getId()); });
assets.forEach((asset) -> {
assetService.deleteAsset(tenantId, asset.getId());
});
}
}
@ -267,10 +276,10 @@ public class AssetServiceTest extends AbstractServiceTest {
@Test
public void testFindAssetsByTenantId() {
List<Asset> assets = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset"+i);
asset.setName("Asset" + i);
asset.setType("default");
assets.add(assetService.saveAsset(asset));
}
@ -303,11 +312,11 @@ public class AssetServiceTest extends AbstractServiceTest {
public void testFindAssetsByTenantIdAndName() {
String title1 = "Asset title 1";
List<AssetInfo> assetsTitle1 = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title1+suffix;
String name = title1 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType("default");
@ -315,11 +324,11 @@ public class AssetServiceTest extends AbstractServiceTest {
}
String title2 = "Asset title 2";
List<AssetInfo> assetsTitle2 = new ArrayList<>();
for (int i=0;i<17;i++) {
for (int i = 0; i < 17; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title2+suffix;
String name = title2 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType("default");
@ -381,11 +390,11 @@ public class AssetServiceTest extends AbstractServiceTest {
String title1 = "Asset title 1";
String type1 = "typeA";
List<Asset> assetsType1 = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title1+suffix;
String name = title1 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType(type1);
@ -394,11 +403,11 @@ public class AssetServiceTest extends AbstractServiceTest {
String title2 = "Asset title 2";
String type2 = "typeB";
List<Asset> assetsType2 = new ArrayList<>();
for (int i=0;i<17;i++) {
for (int i = 0; i < 17; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title2+suffix;
String name = title2 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType(type2);
@ -464,10 +473,10 @@ public class AssetServiceTest extends AbstractServiceTest {
CustomerId customerId = customer.getId();
List<AssetInfo> assets = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset"+i);
asset.setName("Asset" + i);
asset.setType("default");
asset = assetService.saveAsset(asset);
assets.add(new AssetInfo(assetService.assignAssetToCustomer(tenantId, asset.getId(), customerId), customer.getTitle(), customer.isPublic(), "default"));
@ -508,11 +517,11 @@ public class AssetServiceTest extends AbstractServiceTest {
String title1 = "Asset title 1";
List<Asset> assetsTitle1 = new ArrayList<>();
for (int i=0;i<17;i++) {
for (int i = 0; i < 17; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title1+suffix;
String name = title1 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType("default");
@ -521,11 +530,11 @@ public class AssetServiceTest extends AbstractServiceTest {
}
String title2 = "Asset title 2";
List<Asset> assetsTitle2 = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title2+suffix;
String name = title2 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType("default");
@ -596,11 +605,11 @@ public class AssetServiceTest extends AbstractServiceTest {
String title1 = "Asset title 1";
String type1 = "typeC";
List<Asset> assetsType1 = new ArrayList<>();
for (int i=0;i<17;i++) {
for (int i = 0; i < 17; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title1+suffix;
String name = title1 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType(type1);
@ -610,11 +619,11 @@ public class AssetServiceTest extends AbstractServiceTest {
String title2 = "Asset title 2";
String type2 = "typeD";
List<Asset> assetsType2 = new ArrayList<>();
for (int i=0;i<13;i++) {
for (int i = 0; i < 13; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
String suffix = StringUtils.randomAlphanumeric(15);
String name = title2+suffix;
String name = title2 + suffix;
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
asset.setName(name);
asset.setType(type2);
@ -848,4 +857,50 @@ public class AssetServiceTest extends AbstractServiceTest {
);
}
@Test
public void testDeleteAssetIfReferencedInCalculatedField() {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = assetService.saveAsset(asset);
Asset assetWithCf = new Asset();
assetWithCf.setTenantId(tenantId);
assetWithCf.setName("Asset with CF");
assetWithCf.setType("default");
Asset savedAssetWithCf = assetService.saveAsset(assetWithCf);
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setName("Test CF");
calculatedField.setType("Simple");
calculatedField.setEntityId(savedAssetWithCf.getId());
CalculatedFieldConfig config = new CalculatedFieldConfig();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
argument.setEntityId(savedAsset.getId());
argument.setType("TIME_SERIES");
argument.setKey("temperature");
config.setArguments(Map.of("T", argument));
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType("TIME_SERIES");
output.setExpression("T - (100 - H) / 5");
config.setOutput(output);
calculatedField.setConfiguration(config);
CalculatedField savedCalculatedField = calculatedFieldService.save(calculatedField);
assertThatThrownBy(() -> assetService.deleteAsset(tenantId, savedAsset.getId()))
.isInstanceOf(DataValidationException.class)
.hasMessage("Can't delete asset that has entity views or is referenced in calculated fields!");
calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId());
}
}

61
dao/src/test/java/org/thingsboard/server/dao/calculated_field/CalculatedFieldServiceTest.java → dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.calculated_field;
package org.thingsboard.server.dao.service;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@ -21,18 +21,19 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@ -61,7 +62,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Test
public void testSaveCalculatedField() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId());
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
CalculatedField savedCalculatedField = calculatedFieldService.save(calculatedField);
assertThat(savedCalculatedField).isNotNull();
@ -85,7 +86,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Test
public void testSaveCalculatesFieldWithNonExistingDeviceId() {
CalculatedField calculatedField = getCalculatedField(new DeviceId(UUID.fromString("038f8668-c9fd-4f00-8501-ce20f2f93c22")));
DeviceId deviceId = new DeviceId(UUID.fromString("038f8668-c9fd-4f00-8501-ce20f2f93c22"));
CalculatedField calculatedField = getCalculatedField(deviceId, deviceId);
assertThatThrownBy(() -> calculatedFieldService.save(calculatedField))
.isInstanceOf(IllegalArgumentException.class)
@ -95,7 +97,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Test
public void testSaveCalculatedFieldWithExistingName() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId());
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
calculatedFieldService.save(calculatedField);
assertThatThrownBy(() -> calculatedFieldService.save(calculatedField))
@ -106,7 +108,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Test
public void testSaveCalculatedFieldWithExistingExternalId() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId());
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
calculatedField.setExternalId(new CalculatedFieldId(UUID.fromString("2ef69d0a-89cf-4868-86f8-c50551d87ebe")));
calculatedFieldService.save(calculatedField);
@ -148,28 +150,18 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
private CalculatedField saveValidCalculatedField() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId());
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
return calculatedFieldService.save(calculatedField);
}
private CalculatedField getCalculatedField(DeviceId deviceId) {
private CalculatedField getCalculatedField(EntityId entityId, EntityId referencedEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setEntityId(deviceId);
calculatedField.setEntityId(entityId);
calculatedField.setType("Simple");
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(JacksonUtil.toJsonNode("{\n" +
" \"T\": {\n" +
" \"key\": \"temperature\",\n" +
" \"type\": \"TIME_SERIES\"\n" +
" },\n" +
" \"H\": {\n" +
" \"key\": \"humidity\",\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"defaultValue\": 50\n" +
" }\n" +
" }\n"));
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return calculatedField;
}
@ -178,11 +170,30 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setTenantId(tenantId);
calculatedFieldLink.setEntityId(calculatedField.getEntityId());
calculatedFieldLink.setConfiguration(calculatedField.getConfiguration());
// calculatedFieldLink.setConfiguration(calculatedField.getConfiguration());
calculatedFieldLink.setCalculatedFieldId(calculatedField.getId());
return calculatedFieldLink;
}
private CalculatedFieldConfig getCalculatedFieldConfig(EntityId referencedEntityId) {
CalculatedFieldConfig config = new CalculatedFieldConfig();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
argument.setEntityId(referencedEntityId);
argument.setType("TIME_SERIES");
argument.setKey("temperature");
config.setArguments(Map.of("T", argument));
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType("TIME_SERIES");
output.setExpression("T - (100 - H) / 5");
config.setOutput(output);
return config;
}
private Device createTestDevice() {
Device device = new Device();
device.setTenantId(tenantId);

57
dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java

@ -30,14 +30,20 @@ import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@ -45,13 +51,17 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
public class CustomerServiceTest extends AbstractServiceTest {
@Autowired
CustomerService customerService;
@Autowired
CalculatedFieldService calculatedFieldService;
@Autowired
AssetService assetService;
static final int TIMEOUT = 30;
@ -343,4 +353,49 @@ public class CustomerServiceTest extends AbstractServiceTest {
}
}
@Test
public void testDeleteCustomerIfReferencedInCalculatedField() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle("My customer");
Customer savedCustomer = customerService.saveCustomer(customer);
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = assetService.saveAsset(asset);
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setName("Test CF");
calculatedField.setType("Simple");
calculatedField.setEntityId(savedAsset.getId());
CalculatedFieldConfig config = new CalculatedFieldConfig();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
argument.setEntityId(savedCustomer.getId());
argument.setType("TIME_SERIES");
argument.setKey("temperature");
config.setArguments(Map.of("T", argument));
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType("TIME_SERIES");
output.setExpression("T - (100 - H) / 5");
config.setOutput(output);
calculatedField.setConfiguration(config);
CalculatedField savedCalculatedField = calculatedFieldService.save(calculatedField);
assertThatThrownBy(() -> customerService.deleteCustomer(tenantId, savedCustomer.getId()))
.isInstanceOf(DataValidationException.class)
.hasMessage("Can't delete customer that is referenced in calculated fields!");
calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId());
}
}

44
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -39,6 +39,8 @@ import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldConfig;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
@ -50,6 +52,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
@ -64,6 +67,7 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
@ -87,6 +91,8 @@ public class DeviceServiceTest extends AbstractServiceTest {
@Autowired
TenantProfileService tenantProfileService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private PlatformTransactionManager platformTransactionManager;
@SpyBean
private DeviceCredentialsDataValidator validator;
@ -1198,4 +1204,42 @@ public class DeviceServiceTest extends AbstractServiceTest {
);
}
@Test
public void testDeleteAssetIfReferencedInCalculatedField() {
Device device = saveDevice(tenantId, "Test Device");
Device deviceWithCf = saveDevice(tenantId, "Device with CF");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setName("Test CF");
calculatedField.setType("Simple");
calculatedField.setEntityId(deviceWithCf.getId());
CalculatedFieldConfig config = new CalculatedFieldConfig();
CalculatedFieldConfig.Argument argument = new CalculatedFieldConfig.Argument();
argument.setEntityId(device.getId());
argument.setType("TIME_SERIES");
argument.setKey("temperature");
config.setArguments(Map.of("T", argument));
CalculatedFieldConfig.Output output = new CalculatedFieldConfig.Output();
output.setType("TIME_SERIES");
output.setExpression("T - (100 - H) / 5");
config.setOutput(output);
calculatedField.setConfiguration(config);
CalculatedField savedCalculatedField = calculatedFieldService.save(calculatedField);
assertThatThrownBy(() -> deviceService.deleteDevice(tenantId, device.getId()))
.isInstanceOf(DataValidationException.class)
.hasMessage("Can't delete device that has entity views or is referenced in calculated fields!");
calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId());
}
}

5
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java

@ -20,6 +20,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.entity.EntityServiceRegistry;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -44,4 +45,8 @@ public class EntityServiceRegistryTest extends AbstractServiceTest {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService);
}
@Test
public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService);
}
}

4
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java

@ -19,10 +19,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.calculated_field.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldDao;
import org.thingsboard.server.dao.cf.CalculatedFieldDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.UUID;

6
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java

@ -19,11 +19,11 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.calculated_field.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.calculated_field.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.UUID;
@ -54,4 +54,4 @@ public class CalculatedFieldLinkDataValidatorTest {
.hasMessage("Can't update non existing calculated field link!");
}
}
}

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -50,6 +50,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -357,6 +358,8 @@ public interface TbContext {
SlackService getSlackService();
CalculatedFieldService getCalculatedFieldService();
boolean isExternalNodeForceAck();
/**

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java

@ -18,10 +18,13 @@ package org.thingsboard.rule.engine.util;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -157,6 +160,17 @@ public class TenantIdLoader {
case MOBILE_APP:
tenantEntity = ctx.getMobileAppService().findMobileAppById(ctxTenantId, new MobileAppId(id));
break;
case CALCULATED_FIELD:
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id));
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id));
if (calculatedFieldLink != null) {
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId());
} else {
tenantEntity = null;
}
break;
default:
throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType());
}

17
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

@ -43,6 +43,8 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetProfileId;
@ -66,6 +68,7 @@ import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
@ -151,6 +154,8 @@ public class TenantIdLoaderTest {
private DomainService domainService;
@Mock
private MobileAppService mobileAppService;
@Mock
private CalculatedFieldService calculatedFieldService;
private TenantId tenantId;
private TenantProfileId tenantProfileId;
@ -392,6 +397,18 @@ public class TenantIdLoaderTest {
when(ctx.getMobileAppService()).thenReturn(mobileAppService);
doReturn(mobileApp).when(mobileAppService).findMobileAppById(eq(tenantId), any());
break;
case CALCULATED_FIELD:
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any());
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setTenantId(tenantId);
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any());
break;
default:
throw new RuntimeException("Unexpected originator EntityType " + entityType);
}

Loading…
Cancel
Save