From 56059ade0805419f3a77cf9faedabcae8a8a737a Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 30 Sep 2025 13:06:20 +0300 Subject: [PATCH] Rename "Current customer" source to "Current owner" --- ...tractCalculatedFieldProcessingService.java | 34 +-- .../thingsboard/server/cf/AlarmRulesTest.java | 4 +- .../cf/CalculatedFieldCurrentOwnerTest.java | 200 ++++++++++++++++++ .../data/cf/configuration/Argument.java | 2 +- .../CFArgumentDynamicSourceType.java | 2 +- .../CfArgumentDynamicSourceConfiguration.java | 2 +- ...rrentOwnerDynamicSourceConfiguration.java} | 6 +- .../data/cf/configuration/ArgumentTest.java | 4 +- .../ZoneGroupConfigurationTest.java | 6 +- 9 files changed, 216 insertions(+), 44 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{CurrentCustomerDynamicSourceConfiguration.java => CurrentOwnerDynamicSourceConfiguration.java} (78%) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 343db5286f..90d3913263 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -19,13 +19,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; @@ -47,7 +45,6 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -114,7 +111,7 @@ public abstract class AbstractCalculatedFieldProcessingService { if (!argument.hasOwnerSource()) { return entityId; } - return resolveOwnerArgument(tenantId, entityId, argument); + return resolveOwnerArgument(tenantId, entityId); } protected Map resolveArgumentFutures(Map> argFutures) { @@ -166,14 +163,7 @@ public abstract class AbstractCalculatedFieldProcessingService { } var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); return switch (refDynamicSourceConfiguration.getType()) { - case CURRENT_CUSTOMER -> { - EntityId resolved = resolveOwnerArgument(tenantId, entityId, value); - if (resolved != null) { - yield Futures.immediateFuture(List.of(resolved)); - } else { - yield Futures.immediateFuture(Collections.emptyList()); - } - } + case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId))); case RELATION_QUERY -> { var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; if (configuration.isSimpleRelation()) { @@ -192,21 +182,8 @@ public abstract class AbstractCalculatedFieldProcessingService { }; } - @Nullable - private EntityId resolveOwnerArgument(TenantId tenantId, EntityId entityId, Argument argument) { - return switch (argument.getRefDynamicSourceConfiguration().getType()) { - case CURRENT_CUSTOMER -> { - EntityId ownerId = ownerService.getOwner(tenantId, entityId); - if (ownerId.getEntityType() == EntityType.TENANT) { - // todo: if inherit is true - use customer id - // fixme: WTF do we need it at all? - yield null; - } else { - yield ownerId; - } - } - default -> throw new UnsupportedOperationException(); - }; + private EntityId resolveOwnerArgument(TenantId tenantId, EntityId entityId) { + return ownerService.getOwner(tenantId, entityId); } private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { @@ -234,9 +211,6 @@ public abstract class AbstractCalculatedFieldProcessingService { } protected ListenableFuture fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { - if (entityId == null) { - return Futures.immediateFuture(transformSingleValueArgument(Optional.empty())); - } return switch (argument.getRefEntityKey().getType()) { case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs); case ATTRIBUTE -> fetchAttribute(tenantId, entityId, argument, startTs); diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 581fad9a27..9087d9217a 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CurrentCustomerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.event.CalculatedFieldDebugEvent; @@ -218,7 +218,7 @@ public class AlarmRulesTest extends AbstractControllerTest { Argument temperatureThresholdArgument = new Argument(); temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentCustomerDynamicSourceConfiguration()); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); temperatureThresholdArgument.setDefaultValue("1000"); Map arguments = Map.of( diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java new file mode 100644 index 0000000000..d2f9621064 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java @@ -0,0 +1,200 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +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.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest { + + public static final int TIMEOUT = 60; + public static final int POLL_INTERVAL = 1; + + @Test + public void testCreateCFWithCurrentOwner() throws Exception { + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + Device testDevice = createDevice("Test device", "1234567890"); + + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":10}"); + + await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("110"); + }); + } + + @Test + public void testChangeOwner() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + doDelete("/api/customer/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + await().alias("change owner -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + } + + @Test + public void testCreateCFWithCurrentOwnerWhenEntityIsProfile() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); + + Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset1.getId().getId()).andExpect(status().isOk()); + + Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); // owner - TENANT + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(assetProfile.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 1 + ObjectNode result1 = getLatestTelemetry(asset1.getId(), "result"); + assertThat(result1).isNotNull(); + assertThat(result1.get("result").get(0).get("value").asText()).isEqualTo("105"); + + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset2.getId().getId()).andExpect(status().isOk()); + + await().alias("change asset2 owner -> recalculate state for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + } + + private CalculatedField buildCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("Test Calculated Field"); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("attrKey", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument.setRefEntityKey(refEntityKey); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + + config.setArguments(Map.of("a", argument)); + + config.setExpression("a + 100"); + + Output output = new Output(); + output.setName("result"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + config.setOutput(output); + + calculatedField.setConfiguration(config); + return calculatedField; + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index 0aad0737b8..04d926dc2d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -42,7 +42,7 @@ public class Argument { } public boolean hasOwnerSource() { - return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.CURRENT_CUSTOMER; + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.CURRENT_OWNER; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java index e8ef6c7835..3751694eb8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.cf.configuration; public enum CFArgumentDynamicSourceType { - CURRENT_CUSTOMER, + CURRENT_OWNER, RELATION_QUERY } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index c16d8abfcc..639bd18b46 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -27,7 +27,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; ) @JsonSubTypes({ @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), - @JsonSubTypes.Type(value = CurrentCustomerDynamicSourceConfiguration.class, name = "CURRENT_CUSTOMER") + @JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CfArgumentDynamicSourceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentCustomerDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java similarity index 78% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentCustomerDynamicSourceConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java index 8ede2c28df..be9a519f1f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentCustomerDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java @@ -18,13 +18,11 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; @Data -public class CurrentCustomerDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { - - private boolean inherit; // TODO: implement +public class CurrentOwnerDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { @Override public CFArgumentDynamicSourceType getType() { - return CFArgumentDynamicSourceType.CURRENT_CUSTOMER; + return CFArgumentDynamicSourceType.CURRENT_OWNER; } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java index 260a39a8bc..6ac4e63e5f 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -38,9 +38,9 @@ public class ArgumentTest { } @Test - void validateWhenCurrentCustomerSourceConfigurationIsNotNull() { + void validateWhenCurrentOwnerSourceConfigurationIsNotNull() { var argument = new Argument(); - argument.setRefDynamicSourceConfiguration(new CurrentCustomerDynamicSourceConfiguration()); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); assertThat(argument.hasDynamicSource()).isTrue(); assertThat(argument.hasOwnerSource()).isTrue(); assertThat(argument.hasRelationQuerySource()).isFalse(); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java index 7bb657fb33..c2dbc17f57 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CurrentCustomerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -113,9 +113,9 @@ public class ZoneGroupConfigurationTest { } @Test - void whenHasRelationQuerySourceCalled_shouldReturnFalseIfCurrentCustomerSourceConfigured() { + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfCurrentOwnerSourceConfigured() { var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); - zoneGroupConfiguration.setRefDynamicSourceConfiguration(new CurrentCustomerDynamicSourceConfiguration()); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); }